Merge pull request #3594 from NoelDeMartin/MOBILE-2652
MOBILE-2652: Glossary edit & delete entriesmain
commit
28b48ac524
11
gulpfile.js
11
gulpfile.js
|
@ -71,5 +71,14 @@ gulp.task('watch', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('watch-behat', () => {
|
gulp.task('watch-behat', () => {
|
||||||
gulp.watch(['./src/**/*.feature', './src/**/*.png', './local_moodleappbehat'], { interval: 500 }, gulp.parallel('behat'));
|
gulp.watch(
|
||||||
|
[
|
||||||
|
'./src/**/*.feature',
|
||||||
|
'./src/**/tests/behat/fixtures/**',
|
||||||
|
'./src/**/tests/behat/snapshots/**',
|
||||||
|
'./local_moodleappbehat',
|
||||||
|
],
|
||||||
|
{ interval: 500 },
|
||||||
|
gulp.parallel('behat')
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,27 +44,21 @@ class behat_app extends behat_app_helper {
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $featurepath = '';
|
||||||
protected $windowsize = '360x720';
|
protected $windowsize = '360x720';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @BeforeScenario
|
* @BeforeScenario
|
||||||
*/
|
*/
|
||||||
public function before_scenario(ScenarioScope $scope) {
|
public function before_scenario(ScenarioScope $scope) {
|
||||||
if (!$scope->getFeature()->hasTag('app')) {
|
$feature = $scope->getFeature();
|
||||||
|
|
||||||
|
if (!$feature->hasTag('app')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
global $CFG;
|
$this->featurepath = dirname($feature->getFile());
|
||||||
|
$this->configure_performance_logs();
|
||||||
$performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null;
|
|
||||||
|
|
||||||
if ($performanceLogs !== 'ALL') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable DB Logging only for app tests with performance logs activated.
|
|
||||||
$this->getSession()->visit($this->get_app_url() . '/assets/env.json');
|
|
||||||
$this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,6 +83,23 @@ class behat_app extends behat_app_helper {
|
||||||
$this->enter_site();
|
$this->enter_site();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure performance logs.
|
||||||
|
*/
|
||||||
|
protected function configure_performance_logs() {
|
||||||
|
global $CFG;
|
||||||
|
|
||||||
|
$performanceLogs = $CFG->behat_profiles['default']['capabilities']['extra_capabilities']['goog:loggingPrefs']['performance'] ?? null;
|
||||||
|
|
||||||
|
if ($performanceLogs !== 'ALL') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable DB Logging only for app tests with performance logs activated.
|
||||||
|
$this->getSession()->visit($this->get_app_url() . '/assets/env.json');
|
||||||
|
$this->execute_script("document.cookie = 'MoodleAppDBLoggingEnabled=true;path=/';");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the current page is the login form.
|
* Check whether the current page is the login form.
|
||||||
*/
|
*/
|
||||||
|
@ -778,6 +789,35 @@ class behat_app extends behat_app_helper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to a file input, the file path should be relative to a fixtures folder next to the feature file.
|
||||||
|
* The ìnput locator can match a container with a file input inside, it doesn't have to be the input itself.
|
||||||
|
*
|
||||||
|
* @Given /^I upload "((?:[^"]|\\")+)" to (".+") in the app$/
|
||||||
|
* @param string $filename
|
||||||
|
* @param string $inputlocator
|
||||||
|
*/
|
||||||
|
public function i_upload_a_file_in_the_app(string $filename, string $inputlocator) {
|
||||||
|
$filepath = str_replace('/', DIRECTORY_SEPARATOR, "{$this->featurepath}/fixtures/$filename");
|
||||||
|
$inputlocator = $this->parse_element_locator($inputlocator);
|
||||||
|
|
||||||
|
$id = $this->spin(function() use ($inputlocator) {
|
||||||
|
$result = $this->runtime_js("getFileInputId($inputlocator)");
|
||||||
|
|
||||||
|
if (str_starts_with($result, 'ERROR')) {
|
||||||
|
throw new DriverException('Error finding input - ' . $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->wait_for_pending_js();
|
||||||
|
|
||||||
|
$fileinput = $this ->getSession()->getPage()->findById($id);
|
||||||
|
|
||||||
|
$fileinput->attachFile($filepath);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks a field matches a certain value in the app.
|
* Checks a field matches a certain value in the app.
|
||||||
*
|
*
|
||||||
|
|
|
@ -33,7 +33,7 @@ async function main() {
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if (!existsSync(pluginPath)) {
|
if (!existsSync(pluginPath)) {
|
||||||
mkdirSync(pluginPath);
|
mkdirSync(pluginPath, { recursive: true });
|
||||||
} else {
|
} else {
|
||||||
// Empty directory, except the excluding list.
|
// Empty directory, except the excluding list.
|
||||||
const excludeFromErase = [
|
const excludeFromErase = [
|
||||||
|
@ -76,21 +76,29 @@ async function main() {
|
||||||
};
|
};
|
||||||
writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements));
|
writeFileSync(pluginFilePath, replaceArguments(fileContents, replacements));
|
||||||
|
|
||||||
// Copy feature and snapshot files.
|
// Copy features, snapshots, and fixtures.
|
||||||
if (!excludeFeatures) {
|
if (!excludeFeatures) {
|
||||||
const behatTempFeaturesPath = `${pluginPath}/behat-tmp`;
|
const behatTempFeaturesPath = `${pluginPath}/behat-tmp`;
|
||||||
copySync(projectPath('src'), behatTempFeaturesPath, { filter: shouldCopyFileOrDirectory });
|
copySync(projectPath('src'), behatTempFeaturesPath, { filter: shouldCopyFileOrDirectory });
|
||||||
|
|
||||||
const behatFeaturesPath = `${pluginPath}/tests/behat`;
|
const behatFeaturesPath = `${pluginPath}/tests/behat`;
|
||||||
if (!existsSync(behatFeaturesPath)) {
|
if (!existsSync(behatFeaturesPath)) {
|
||||||
mkdirSync(behatFeaturesPath, {recursive: true});
|
mkdirSync(behatFeaturesPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const file of getDirectoryFiles(behatTempFeaturesPath)) {
|
for await (const file of getDirectoryFiles(behatTempFeaturesPath)) {
|
||||||
const filePath = dirname(file);
|
const filePath = dirname(file);
|
||||||
|
const snapshotsIndex = file.indexOf('/tests/behat/snapshots/');
|
||||||
|
const fixturesIndex = file.indexOf('/tests/behat/fixtures/');
|
||||||
|
|
||||||
if (filePath.endsWith('/tests/behat/snapshots')) {
|
if (snapshotsIndex !== -1) {
|
||||||
renameSync(file, behatFeaturesPath + '/snapshots/' + basename(file));
|
moveFile(file, behatFeaturesPath + '/snapshots/' + file.slice(snapshotsIndex + 23));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fixturesIndex !== -1) {
|
||||||
|
moveFile(file, behatFeaturesPath + '/fixtures/' + file.slice(fixturesIndex + 22));
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -103,7 +111,7 @@ async function main() {
|
||||||
const searchRegExp = /\//g;
|
const searchRegExp = /\//g;
|
||||||
const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core';
|
const prefix = relative(behatTempFeaturesPath, newPath).replace(searchRegExp,'-') || 'core';
|
||||||
const featureFilename = prefix + '-' + basename(file);
|
const featureFilename = prefix + '-' + basename(file);
|
||||||
renameSync(file, behatFeaturesPath + '/' + featureFilename);
|
moveFile(file, behatFeaturesPath + '/' + featureFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
rmSync(behatTempFeaturesPath, {recursive: true});
|
rmSync(behatTempFeaturesPath, {recursive: true});
|
||||||
|
@ -115,7 +123,8 @@ function shouldCopyFileOrDirectory(path) {
|
||||||
|
|
||||||
return stats.isDirectory()
|
return stats.isDirectory()
|
||||||
|| extname(path) === '.feature'
|
|| extname(path) === '.feature'
|
||||||
|| extname(path) === '.png';
|
|| path.includes('/tests/behat/snapshots')
|
||||||
|
|| path.includes('/tests/behat/fixtures');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isExcluded(file, exclusions) {
|
function isExcluded(file, exclusions) {
|
||||||
|
@ -127,6 +136,16 @@ function fail(message) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function moveFile(from, to) {
|
||||||
|
const targetDir = dirname(to);
|
||||||
|
|
||||||
|
if (!existsSync(targetDir)) {
|
||||||
|
mkdirSync(targetDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
renameSync(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
function guessPluginPath() {
|
function guessPluginPath() {
|
||||||
if (process.env.MOODLE_APP_BEHAT_PLUGIN_PATH) {
|
if (process.env.MOODLE_APP_BEHAT_PLUGIN_PATH) {
|
||||||
return process.env.MOODLE_APP_BEHAT_PLUGIN_PATH;
|
return process.env.MOODLE_APP_BEHAT_PLUGIN_PATH;
|
||||||
|
|
|
@ -681,6 +681,7 @@
|
||||||
"addon.mod_forum.yourreply": "forum",
|
"addon.mod_forum.yourreply": "forum",
|
||||||
"addon.mod_glossary.addentry": "glossary",
|
"addon.mod_glossary.addentry": "glossary",
|
||||||
"addon.mod_glossary.aliases": "glossary",
|
"addon.mod_glossary.aliases": "glossary",
|
||||||
|
"addon.mod_glossary.areyousuredelete": "glossary",
|
||||||
"addon.mod_glossary.attachment": "glossary",
|
"addon.mod_glossary.attachment": "glossary",
|
||||||
"addon.mod_glossary.browsemode": "local_moodlemobileapp",
|
"addon.mod_glossary.browsemode": "local_moodlemobileapp",
|
||||||
"addon.mod_glossary.byalphabet": "local_moodlemobileapp",
|
"addon.mod_glossary.byalphabet": "local_moodlemobileapp",
|
||||||
|
@ -694,9 +695,14 @@
|
||||||
"addon.mod_glossary.categories": "glossary",
|
"addon.mod_glossary.categories": "glossary",
|
||||||
"addon.mod_glossary.concept": "glossary",
|
"addon.mod_glossary.concept": "glossary",
|
||||||
"addon.mod_glossary.definition": "glossary",
|
"addon.mod_glossary.definition": "glossary",
|
||||||
|
"addon.mod_glossary.deleteentry": "glossary",
|
||||||
|
"addon.mod_glossary.editentry": "glossary",
|
||||||
"addon.mod_glossary.entriestobesynced": "local_moodlemobileapp",
|
"addon.mod_glossary.entriestobesynced": "local_moodlemobileapp",
|
||||||
|
"addon.mod_glossary.entry": "glossary",
|
||||||
|
"addon.mod_glossary.entrydeleted": "glossary",
|
||||||
"addon.mod_glossary.entrypendingapproval": "local_moodlemobileapp",
|
"addon.mod_glossary.entrypendingapproval": "local_moodlemobileapp",
|
||||||
"addon.mod_glossary.entryusedynalink": "glossary",
|
"addon.mod_glossary.entryusedynalink": "glossary",
|
||||||
|
"addon.mod_glossary.errordeleting": "local_moodlemobileapp",
|
||||||
"addon.mod_glossary.errconceptalreadyexists": "glossary",
|
"addon.mod_glossary.errconceptalreadyexists": "glossary",
|
||||||
"addon.mod_glossary.errorloadingentries": "local_moodlemobileapp",
|
"addon.mod_glossary.errorloadingentries": "local_moodlemobileapp",
|
||||||
"addon.mod_glossary.errorloadingentry": "local_moodlemobileapp",
|
"addon.mod_glossary.errorloadingentry": "local_moodlemobileapp",
|
||||||
|
|
|
@ -29,8 +29,6 @@ import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '../servic
|
||||||
*/
|
*/
|
||||||
export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<AddonModGlossaryEntryItem> {
|
export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<AddonModGlossaryEntryItem> {
|
||||||
|
|
||||||
static readonly NEW_ENTRY: AddonModGlossaryNewEntryForm = { newEntry: true };
|
|
||||||
|
|
||||||
readonly COURSE_ID: number;
|
readonly COURSE_ID: number;
|
||||||
readonly CM_ID: number;
|
readonly CM_ID: number;
|
||||||
readonly GLOSSARY_PATH_PREFIX: string;
|
readonly GLOSSARY_PATH_PREFIX: string;
|
||||||
|
@ -54,16 +52,6 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
|
||||||
this.GLOSSARY_PATH_PREFIX = glossaryPathPrefix;
|
this.GLOSSARY_PATH_PREFIX = glossaryPathPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard to infer NewEntryForm objects.
|
|
||||||
*
|
|
||||||
* @param entry Item to check.
|
|
||||||
* @returns Whether the item is a new entry form.
|
|
||||||
*/
|
|
||||||
isNewEntryForm(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryNewEntryForm {
|
|
||||||
return 'newEntry' in entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type guard to infer entry objects.
|
* Type guard to infer entry objects.
|
||||||
*
|
*
|
||||||
|
@ -81,38 +69,28 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
|
||||||
* @returns Whether the item is an offline entry.
|
* @returns Whether the item is an offline entry.
|
||||||
*/
|
*/
|
||||||
isOfflineEntry(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryOfflineEntry {
|
isOfflineEntry(entry: AddonModGlossaryEntryItem): entry is AddonModGlossaryOfflineEntry {
|
||||||
return !this.isNewEntryForm(entry) && !this.isOnlineEntry(entry);
|
return !this.isOnlineEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
getItemPath(entry: AddonModGlossaryEntryItem): string {
|
getItemPath(entry: AddonModGlossaryEntryItem): string {
|
||||||
if (this.isOnlineEntry(entry)) {
|
|
||||||
return `${this.GLOSSARY_PATH_PREFIX}entry/${entry.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isOfflineEntry(entry)) {
|
if (this.isOfflineEntry(entry)) {
|
||||||
return `${this.GLOSSARY_PATH_PREFIX}edit/${entry.timecreated}`;
|
return `${this.GLOSSARY_PATH_PREFIX}entry/new-${entry.timecreated}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${this.GLOSSARY_PATH_PREFIX}edit/0`;
|
return `${this.GLOSSARY_PATH_PREFIX}entry/${entry.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
getItemQueryParams(entry: AddonModGlossaryEntryItem): Params {
|
getItemQueryParams(): Params {
|
||||||
const params: Params = {
|
return {
|
||||||
cmId: this.CM_ID,
|
cmId: this.CM_ID,
|
||||||
courseId: this.COURSE_ID,
|
courseId: this.COURSE_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.isOfflineEntry(entry)) {
|
|
||||||
params.concept = entry.concept;
|
|
||||||
}
|
|
||||||
|
|
||||||
return params;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -164,21 +142,8 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
|
||||||
|
|
||||||
const glossaryId = this.glossary.id;
|
const glossaryId = this.glossary.id;
|
||||||
|
|
||||||
this.fetchFunction = (options) => AddonModGlossary.getEntriesBySearch(
|
this.fetchFunction = (options) => AddonModGlossary.getEntriesBySearch(glossaryId, query, true, options);
|
||||||
glossaryId,
|
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesBySearch(glossaryId, query, true);
|
||||||
query,
|
|
||||||
true,
|
|
||||||
'CONCEPT',
|
|
||||||
'ASC',
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesBySearch(
|
|
||||||
glossaryId,
|
|
||||||
query,
|
|
||||||
true,
|
|
||||||
'CONCEPT',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
this.hasSearched = true;
|
this.hasSearched = true;
|
||||||
this.setDirty(true);
|
this.setDirty(true);
|
||||||
}
|
}
|
||||||
|
@ -192,12 +157,14 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate glossary cache.
|
* Invalidate glossary cache.
|
||||||
|
*
|
||||||
|
* @param invalidateGlossary Whether to invalidate the entire glossary or not
|
||||||
*/
|
*/
|
||||||
async invalidateCache(): Promise<void> {
|
async invalidateCache(invalidateGlossary: boolean = true): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all<unknown>([
|
||||||
AddonModGlossary.invalidateCourseGlossaries(this.COURSE_ID),
|
|
||||||
this.fetchInvalidate && this.fetchInvalidate(),
|
this.fetchInvalidate && this.fetchInvalidate(),
|
||||||
this.glossary && AddonModGlossary.invalidateCategories(this.glossary.id),
|
invalidateGlossary && AddonModGlossary.invalidateCourseGlossaries(this.COURSE_ID),
|
||||||
|
invalidateGlossary && this.glossary && AddonModGlossary.invalidateCategories(this.glossary.id),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,65 +187,29 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
|
||||||
case 'author_all':
|
case 'author_all':
|
||||||
// Browse by author.
|
// Browse by author.
|
||||||
this.viewMode = 'author';
|
this.viewMode = 'author';
|
||||||
this.fetchFunction = (options) => AddonModGlossary.getEntriesByAuthor(
|
this.fetchFunction = (options) => AddonModGlossary.getEntriesByAuthor(glossaryId, options);
|
||||||
glossaryId,
|
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByAuthor(glossaryId);
|
||||||
'ALL',
|
|
||||||
'LASTNAME',
|
|
||||||
'ASC',
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByAuthor(
|
|
||||||
glossaryId,
|
|
||||||
'ALL',
|
|
||||||
'LASTNAME',
|
|
||||||
'ASC',
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'cat_all':
|
case 'cat_all':
|
||||||
// Browse by category.
|
// Browse by category.
|
||||||
this.viewMode = 'cat';
|
this.viewMode = 'cat';
|
||||||
this.fetchFunction = (options) => AddonModGlossary.getEntriesByCategory(
|
this.fetchFunction = (options) => AddonModGlossary.getEntriesByCategory(glossaryId, options);
|
||||||
glossaryId,
|
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByCategory(glossaryId);
|
||||||
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByCategory(
|
|
||||||
glossaryId,
|
|
||||||
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'newest_first':
|
case 'newest_first':
|
||||||
// Newest first.
|
// Newest first.
|
||||||
this.viewMode = 'date';
|
this.viewMode = 'date';
|
||||||
this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(
|
this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(glossaryId, 'CREATION', options);
|
||||||
glossaryId,
|
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION');
|
||||||
'CREATION',
|
|
||||||
'DESC',
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(
|
|
||||||
glossaryId,
|
|
||||||
'CREATION',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'recently_updated':
|
case 'recently_updated':
|
||||||
// Recently updated.
|
// Recently updated.
|
||||||
this.viewMode = 'date';
|
this.viewMode = 'date';
|
||||||
this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(
|
this.fetchFunction = (options) => AddonModGlossary.getEntriesByDate(glossaryId, 'UPDATE', options);
|
||||||
glossaryId,
|
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE');
|
||||||
'UPDATE',
|
|
||||||
'DESC',
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByDate(
|
|
||||||
glossaryId,
|
|
||||||
'UPDATE',
|
|
||||||
'DESC',
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'letter_all':
|
case 'letter_all':
|
||||||
|
@ -286,15 +217,8 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
|
||||||
// Consider it is 'letter_all'.
|
// Consider it is 'letter_all'.
|
||||||
this.viewMode = 'letter';
|
this.viewMode = 'letter';
|
||||||
this.fetchMode = 'letter_all';
|
this.fetchMode = 'letter_all';
|
||||||
this.fetchFunction = (options) => AddonModGlossary.getEntriesByLetter(
|
this.fetchFunction = (options) => AddonModGlossary.getEntriesByLetter(glossaryId, options);
|
||||||
glossaryId,
|
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByLetter(glossaryId);
|
||||||
'ALL',
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
this.fetchInvalidate = () => AddonModGlossary.invalidateEntriesByLetter(
|
|
||||||
glossaryId,
|
|
||||||
'ALL',
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,11 +237,10 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
|
||||||
const entries: AddonModGlossaryEntryItem[] = [];
|
const entries: AddonModGlossaryEntryItem[] = [];
|
||||||
|
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
const offlineEntries = await AddonModGlossaryOffline.getGlossaryNewEntries(glossary.id);
|
const offlineEntries = await AddonModGlossaryOffline.getGlossaryOfflineEntries(glossary.id);
|
||||||
|
|
||||||
offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept));
|
offlineEntries.sort((a, b) => a.concept.localeCompare(b.concept));
|
||||||
|
|
||||||
entries.push(AddonModGlossaryEntriesSource.NEW_ENTRY);
|
|
||||||
entries.push(...offlineEntries);
|
entries.push(...offlineEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,12 +292,7 @@ export class AddonModGlossaryEntriesSource extends CoreRoutedItemsManagerSource<
|
||||||
/**
|
/**
|
||||||
* Type of items that can be held by the entries manager.
|
* Type of items that can be held by the entries manager.
|
||||||
*/
|
*/
|
||||||
export type AddonModGlossaryEntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | AddonModGlossaryNewEntryForm;
|
export type AddonModGlossaryEntryItem = AddonModGlossaryEntry | AddonModGlossaryOfflineEntry;
|
||||||
|
|
||||||
/**
|
|
||||||
* Type to select the new entry form.
|
|
||||||
*/
|
|
||||||
export type AddonModGlossaryNewEntryForm = { newEntry: true };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch mode to sort entries.
|
* Fetch mode to sort entries.
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
|
||||||
import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from './glossary-entries-source';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to manage swiping within a collection of glossary entries.
|
|
||||||
*/
|
|
||||||
export abstract class AddonModGlossaryEntriesSwipeManager
|
|
||||||
extends CoreSwipeNavigationItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
protected skipItemInSwipe(item: AddonModGlossaryEntryItem): boolean {
|
|
||||||
return this.getSource().isNewEntryForm(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -31,7 +31,7 @@
|
||||||
[componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()">
|
[componentId]="componentId" [courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()">
|
||||||
</core-course-module-info>
|
</core-course-module-info>
|
||||||
|
|
||||||
<ion-list *ngIf="!isSearch && entries && entries.offlineEntries.length > 0">
|
<ion-list *ngIf="!isSearch && entries && entries.offlineEntries.length > 0" class="addon-mod-glossary-index--offline-entries">
|
||||||
<ion-item-divider>
|
<ion-item-divider>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2 class="big">{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2>
|
<h2 class="big">{{ 'addon.mod_glossary.entriestobesynced' | translate }}</h2>
|
||||||
|
@ -40,9 +40,12 @@
|
||||||
<ion-item *ngFor="let entry of entries.offlineEntries" (click)="entries.select(entry)" detail="false" button
|
<ion-item *ngFor="let entry of entries.offlineEntries" (click)="entries.select(entry)" detail="false" button
|
||||||
[attr.aria-current]="entries.getItemAriaCurrent(entry)">
|
[attr.aria-current]="entries.getItemAriaCurrent(entry)">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="glossary!.coursemodule"
|
<div class="addon-mod-glossary-index--offline-entry">
|
||||||
[courseId]="courseId">
|
<core-format-text [text]="entry.concept" contextLevel="module" [contextInstanceId]="glossary!.coursemodule"
|
||||||
</core-format-text>
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
<ion-icon name="fas-rotate" class="ion-margin-start" aria-hidden="true"></ion-icon>
|
||||||
|
</div>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
:host {
|
||||||
|
|
||||||
|
.addon-mod-glossary-index--offline-entries {
|
||||||
|
border-bottom: 1px solid var(--stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-mod-glossary-index--offline-entry {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import { CoreRatingProvider } from '@features/rating/services/rating';
|
||||||
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
|
||||||
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
|
||||||
import { IonContent } from '@ionic/angular';
|
import { IonContent } from '@ionic/angular';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
@ -42,12 +43,15 @@ import {
|
||||||
AddonModGlossaryEntryWithCategory,
|
AddonModGlossaryEntryWithCategory,
|
||||||
AddonModGlossaryGlossary,
|
AddonModGlossaryGlossary,
|
||||||
AddonModGlossaryProvider,
|
AddonModGlossaryProvider,
|
||||||
|
GLOSSARY_ENTRY_ADDED,
|
||||||
|
GLOSSARY_ENTRY_DELETED,
|
||||||
|
GLOSSARY_ENTRY_UPDATED,
|
||||||
} from '../../services/glossary';
|
} from '../../services/glossary';
|
||||||
import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
import { AddonModGlossaryOfflineEntry } from '../../services/glossary-offline';
|
||||||
import {
|
import {
|
||||||
AddonModGlossaryAutoSyncData,
|
AddonModGlossaryAutoSyncedData,
|
||||||
AddonModGlossarySyncProvider,
|
|
||||||
AddonModGlossarySyncResult,
|
AddonModGlossarySyncResult,
|
||||||
|
GLOSSARY_AUTO_SYNCED,
|
||||||
} from '../../services/glossary-sync';
|
} from '../../services/glossary-sync';
|
||||||
import { AddonModGlossaryModuleHandlerService } from '../../services/handlers/module';
|
import { AddonModGlossaryModuleHandlerService } from '../../services/handlers/module';
|
||||||
import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch';
|
import { AddonModGlossaryPrefetchHandler } from '../../services/handlers/prefetch';
|
||||||
|
@ -59,6 +63,7 @@ import { AddonModGlossaryModePickerPopoverComponent } from '../mode-picker/mode-
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'addon-mod-glossary-index',
|
selector: 'addon-mod-glossary-index',
|
||||||
templateUrl: 'addon-mod-glossary-index.html',
|
templateUrl: 'addon-mod-glossary-index.html',
|
||||||
|
styleUrls: ['index.scss'],
|
||||||
})
|
})
|
||||||
export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent
|
export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivityComponent
|
||||||
implements OnInit, AfterViewInit, OnDestroy {
|
implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
@ -75,13 +80,11 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
|
|
||||||
protected hasOfflineEntries = false;
|
protected hasOfflineEntries = false;
|
||||||
protected hasOfflineRatings = false;
|
protected hasOfflineRatings = false;
|
||||||
protected syncEventName = AddonModGlossarySyncProvider.AUTO_SYNCED;
|
protected syncEventName = GLOSSARY_AUTO_SYNCED;
|
||||||
protected addEntryObserver?: CoreEventObserver;
|
|
||||||
protected fetchedEntriesCanLoadMore = false;
|
protected fetchedEntriesCanLoadMore = false;
|
||||||
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
protected fetchedEntries: AddonModGlossaryEntry[] = [];
|
||||||
protected sourceUnsubscribe?: () => void;
|
protected sourceUnsubscribe?: () => void;
|
||||||
protected ratingOfflineObserver?: CoreEventObserver;
|
protected observers?: CoreEventObserver[];
|
||||||
protected ratingSyncObserver?: CoreEventObserver;
|
|
||||||
protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead.
|
protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead.
|
||||||
|
|
||||||
getDivider?: (entry: AddonModGlossaryEntry) => string;
|
getDivider?: (entry: AddonModGlossaryEntry) => string;
|
||||||
|
@ -136,30 +139,48 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
});
|
});
|
||||||
|
|
||||||
// When an entry is added, we reload the data.
|
// When an entry is added, we reload the data.
|
||||||
this.addEntryObserver = CoreEvents.on(AddonModGlossaryProvider.ADD_ENTRY_EVENT, (data) => {
|
this.observers = [
|
||||||
if (this.glossary && this.glossary.id === data.glossaryId) {
|
CoreEvents.on(GLOSSARY_ENTRY_ADDED, ({ glossaryId }) => {
|
||||||
this.showLoadingAndRefresh(false);
|
if (this.glossary?.id !== glossaryId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check completion since it could be configured to complete once the user adds a new entry.
|
// Check completion since it could be configured to complete once the user adds a new entry.
|
||||||
this.checkCompletion();
|
this.checkCompletion();
|
||||||
}
|
|
||||||
});
|
this.showLoadingAndRefresh(false);
|
||||||
|
}),
|
||||||
|
CoreEvents.on(GLOSSARY_ENTRY_UPDATED, ({ glossaryId }) => {
|
||||||
|
if (this.glossary?.id !== glossaryId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showLoadingAndRefresh(false);
|
||||||
|
}),
|
||||||
|
CoreEvents.on(GLOSSARY_ENTRY_DELETED, ({ glossaryId }) => {
|
||||||
|
if (this.glossary?.id !== glossaryId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showLoadingAndRefresh(false);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
// Listen for offline ratings saved and synced.
|
// Listen for offline ratings saved and synced.
|
||||||
this.ratingOfflineObserver = CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
|
this.observers.push(CoreEvents.on(CoreRatingProvider.RATING_SAVED_EVENT, (data) => {
|
||||||
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
||||||
&& data.instanceId == this.glossary.coursemodule) {
|
&& data.instanceId == this.glossary.coursemodule) {
|
||||||
this.hasOfflineRatings = true;
|
this.hasOfflineRatings = true;
|
||||||
this.hasOffline = true;
|
this.hasOffline = true;
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
this.ratingSyncObserver = CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
|
this.observers.push(CoreEvents.on(CoreRatingSyncProvider.SYNCED_EVENT, (data) => {
|
||||||
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
if (this.glossary && data.component == 'mod_glossary' && data.ratingArea == 'entry' && data.contextLevel == 'module'
|
||||||
&& data.instanceId == this.glossary.coursemodule) {
|
&& data.instanceId == this.glossary.coursemodule) {
|
||||||
this.hasOfflineRatings = false;
|
this.hasOfflineRatings = false;
|
||||||
this.hasOffline = this.hasOfflineEntries;
|
this.hasOffline = this.hasOfflineEntries;
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -227,7 +248,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* @param syncEventData Data receiven on sync observer.
|
* @param syncEventData Data receiven on sync observer.
|
||||||
* @returns True if refresh is needed, false otherwise.
|
* @returns True if refresh is needed, false otherwise.
|
||||||
*/
|
*/
|
||||||
protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncData): boolean {
|
protected isRefreshSyncNeeded(syncEventData: AddonModGlossaryAutoSyncedData): boolean {
|
||||||
return !!this.glossary && syncEventData.glossaryId == this.glossary.id &&
|
return !!this.glossary && syncEventData.glossaryId == this.glossary.id &&
|
||||||
syncEventData.userId == CoreSites.getCurrentSiteUserId();
|
syncEventData.userId == CoreSites.getCurrentSiteUserId();
|
||||||
}
|
}
|
||||||
|
@ -388,7 +409,11 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
* Opens new entry editor.
|
* Opens new entry editor.
|
||||||
*/
|
*/
|
||||||
openNewEntry(): void {
|
openNewEntry(): void {
|
||||||
this.entries?.select(AddonModGlossaryEntriesSource.NEW_ENTRY);
|
CoreNavigator.navigate(
|
||||||
|
this.splitView.outletActivated
|
||||||
|
? '../new'
|
||||||
|
: './entry/new',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -410,9 +435,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
super.ngOnDestroy();
|
super.ngOnDestroy();
|
||||||
|
|
||||||
this.addEntryObserver?.off();
|
this.observers?.forEach(observer => observer.off());
|
||||||
this.ratingOfflineObserver?.off();
|
|
||||||
this.ratingSyncObserver?.off();
|
|
||||||
this.sourceUnsubscribe?.call(null);
|
this.sourceUnsubscribe?.call(null);
|
||||||
this.entries?.destroy();
|
this.entries?.destroy();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,13 +27,9 @@ const mobileRoutes: Routes = [
|
||||||
component: AddonModGlossaryIndexPage,
|
component: AddonModGlossaryIndexPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':courseId/:cmId/entry/:entryId',
|
path: ':courseId/:cmId/entry/:entrySlug',
|
||||||
loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule),
|
loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ':courseId/:cmId/edit/:timecreated',
|
|
||||||
loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const tabletRoutes: Routes = [
|
const tabletRoutes: Routes = [
|
||||||
|
@ -42,18 +38,22 @@ const tabletRoutes: Routes = [
|
||||||
component: AddonModGlossaryIndexPage,
|
component: AddonModGlossaryIndexPage,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'entry/:entryId',
|
path: 'entry/:entrySlug',
|
||||||
loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule),
|
loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'edit/:timecreated',
|
|
||||||
loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: ':courseId/:cmId/entry/new',
|
||||||
|
loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':courseId/:cmId/entry/:entrySlug/edit',
|
||||||
|
loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
|
||||||
|
},
|
||||||
...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile),
|
...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile),
|
||||||
...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet),
|
...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet),
|
||||||
];
|
];
|
||||||
|
|
|
@ -49,50 +49,40 @@ export const ADDON_MOD_GLOSSARY_SERVICES: Type<unknown>[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const mainMenuRoutes: Routes = [
|
const mainMenuRoutes: Routes = [
|
||||||
{
|
// Course activity navigation.
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
|
||||||
loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule),
|
|
||||||
data: { swipeEnabled: false },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
|
||||||
loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
|
|
||||||
data: { swipeEnabled: false },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
|
path: AddonModGlossaryModuleHandlerService.PAGE_NAME,
|
||||||
loadChildren: () => import('./glossary-lazy.module').then(m => m.AddonModGlossaryLazyModule),
|
loadChildren: () => import('./glossary-lazy.module').then(m => m.AddonModGlossaryLazyModule),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Single Activity format navigation.
|
||||||
|
{
|
||||||
|
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/new`,
|
||||||
|
loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entrySlug/edit`,
|
||||||
|
loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
|
||||||
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
|
},
|
||||||
...conditionalRoutes(
|
...conditionalRoutes(
|
||||||
[
|
[{
|
||||||
{
|
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entrySlug`,
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule),
|
||||||
loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule),
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
}],
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${COURSE_CONTENTS_PATH}/${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
|
||||||
loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
|
|
||||||
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
() => CoreScreen.isMobile,
|
() => CoreScreen.isMobile,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Single Activity format navigation.
|
||||||
const courseContentsRoutes: Routes = conditionalRoutes(
|
const courseContentsRoutes: Routes = conditionalRoutes(
|
||||||
[
|
[{
|
||||||
{
|
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entrySlug`,
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/entry/:entryId`,
|
loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule),
|
||||||
loadChildren: () => import('./glossary-entry-lazy.module').then(m => m.AddonModGlossaryEntryLazyModule),
|
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
||||||
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
}],
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/edit/:timecreated`,
|
|
||||||
loadChildren: () => import('./glossary-edit-lazy.module').then(m => m.AddonModGlossaryEditLazyModule),
|
|
||||||
data: { glossaryPathPrefix: `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
() => CoreScreen.isTablet,
|
() => CoreScreen.isTablet,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"addentry": "Add a new entry",
|
"addentry": "Add a new entry",
|
||||||
"aliases": "Keyword(s)",
|
"aliases": "Keyword(s)",
|
||||||
|
"areyousuredelete": "Are you sure you want to delete this entry?",
|
||||||
"attachment": "Attachment",
|
"attachment": "Attachment",
|
||||||
"browsemode": "Browse entries",
|
"browsemode": "Browse entries",
|
||||||
"byalphabet": "Alphabetically",
|
"byalphabet": "Alphabetically",
|
||||||
|
@ -14,10 +15,15 @@
|
||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"concept": "Concept",
|
"concept": "Concept",
|
||||||
"definition": "Definition",
|
"definition": "Definition",
|
||||||
|
"deleteentry": "Delete entry",
|
||||||
|
"editentry": "Edit entry",
|
||||||
"entriestobesynced": "Entries to be synced",
|
"entriestobesynced": "Entries to be synced",
|
||||||
|
"entry": "Entry",
|
||||||
|
"entrydeleted": "Entry deleted",
|
||||||
"entrypendingapproval": "This entry is pending approval.",
|
"entrypendingapproval": "This entry is pending approval.",
|
||||||
"entryusedynalink": "This entry should be automatically linked",
|
"entryusedynalink": "This entry should be automatically linked",
|
||||||
"errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.",
|
"errconceptalreadyexists": "This concept already exists. No duplicates allowed in this glossary.",
|
||||||
|
"errordeleting": "Error deleting entry.",
|
||||||
"errorloadingentries": "An error occurred while loading entries.",
|
"errorloadingentries": "An error occurred while loading entries.",
|
||||||
"errorloadingentry": "An error occurred while loading the entry.",
|
"errorloadingentry": "An error occurred while loading the entry.",
|
||||||
"errorloadingglossary": "An error occurred while loading the glossary.",
|
"errorloadingglossary": "An error occurred while loading the glossary.",
|
||||||
|
|
|
@ -11,12 +11,12 @@
|
||||||
</ion-title>
|
</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content [core-swipe-navigation]="entries">
|
<ion-content>
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<form #editFormEl *ngIf="glossary">
|
<form #editFormEl *ngIf="glossary">
|
||||||
<ion-item>
|
<ion-item>
|
||||||
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
|
<ion-label position="stacked">{{ 'addon.mod_glossary.concept' | translate }}</ion-label>
|
||||||
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="entry.concept" name="concept">
|
<ion-input type="text" [placeholder]="'addon.mod_glossary.concept' | translate" [(ngModel)]="data.concept" name="concept">
|
||||||
</ion-input>
|
</ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<ion-label position="stacked">
|
<ion-label position="stacked">
|
||||||
{{ 'addon.mod_glossary.categories' | translate }}
|
{{ 'addon.mod_glossary.categories' | translate }}
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-select [(ngModel)]="options.categories" multiple="true" interface="action-sheet"
|
<ion-select [(ngModel)]="data.categories" multiple="true" interface="action-sheet"
|
||||||
[placeholder]="'addon.mod_glossary.categories' | translate" name="categories"
|
[placeholder]="'addon.mod_glossary.categories' | translate" name="categories"
|
||||||
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
|
[interfaceOptions]="{header: 'addon.mod_glossary.categories' | translate}">
|
||||||
<ion-select-option *ngFor="let category of categories" [value]="category.id">
|
<ion-select-option *ngFor="let category of categories" [value]="category.id">
|
||||||
|
@ -39,11 +39,11 @@
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
</ion-select>
|
</ion-select>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
<ion-item *ngIf="showAliases">
|
||||||
<ion-label position="stacked">
|
<ion-label position="stacked">
|
||||||
{{ 'addon.mod_glossary.aliases' | translate }}
|
{{ 'addon.mod_glossary.aliases' | translate }}
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-textarea [(ngModel)]="options.aliases" rows="1" [core-auto-rows]="options.aliases" name="aliases">
|
<ion-textarea [(ngModel)]="data.aliases" rows="1" [core-auto-rows]="data.aliases" name="aliases">
|
||||||
</ion-textarea>
|
</ion-textarea>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item-divider>
|
<ion-item-divider>
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
<h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
|
<h2>{{ 'addon.mod_glossary.attachment' | translate }}</h2>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<core-attachments [files]="attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
|
<core-attachments [files]="data.attachments" [component]="component" [componentId]="glossary.coursemodule" [allowOffline]="true"
|
||||||
[courseId]="courseId">
|
[courseId]="courseId">
|
||||||
</core-attachments>
|
</core-attachments>
|
||||||
<ng-container *ngIf="glossary.usedynalink">
|
<ng-container *ngIf="glossary.usedynalink">
|
||||||
|
@ -62,19 +62,19 @@
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
|
<ion-label>{{ 'addon.mod_glossary.entryusedynalink' | translate }}</ion-label>
|
||||||
<ion-toggle [(ngModel)]="options.usedynalink" name="usedynalink"></ion-toggle>
|
<ion-toggle [(ngModel)]="data.usedynalink" name="usedynalink"></ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
|
<ion-label>{{ 'addon.mod_glossary.casesensitive' | translate }}</ion-label>
|
||||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.casesensitive" name="casesensitive">
|
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.casesensitive" name="casesensitive">
|
||||||
</ion-toggle>
|
</ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
|
<ion-label>{{ 'addon.mod_glossary.fullmatch' | translate }}</ion-label>
|
||||||
<ion-toggle [disabled]="!options.usedynalink" [(ngModel)]="options.fullmatch" name="fullmatch"></ion-toggle>
|
<ion-toggle [disabled]="!data.usedynalink" [(ngModel)]="data.fullmatch" name="fullmatch"></ion-toggle>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ion-button class="ion-margin" expand="block" [disabled]="!entry.concept || !entry.definition" (click)="save()">
|
<ion-button class="ion-margin" expand="block" [disabled]="!data.concept || !data.definition" (click)="save()">
|
||||||
{{ 'core.save' | translate }}
|
{{ 'core.save' | translate }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -12,16 +12,17 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, ViewChild, ElementRef, Optional, OnDestroy } from '@angular/core';
|
import { Component, OnInit, ViewChild, ElementRef, Optional } from '@angular/core';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CanLeave } from '@guards/can-leave';
|
import { CanLeave } from '@guards/can-leave';
|
||||||
import { FileEntry } from '@ionic-native/file/ngx';
|
import { CoreFileEntry } from '@services/file-helper';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreNetwork } from '@services/network';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
@ -29,15 +30,12 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreForms } from '@singletons/form';
|
import { CoreForms } from '@singletons/form';
|
||||||
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
|
||||||
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
|
||||||
import {
|
import {
|
||||||
AddonModGlossary,
|
AddonModGlossary,
|
||||||
AddonModGlossaryCategory,
|
AddonModGlossaryCategory,
|
||||||
|
AddonModGlossaryEntry,
|
||||||
AddonModGlossaryEntryOption,
|
AddonModGlossaryEntryOption,
|
||||||
AddonModGlossaryGlossary,
|
AddonModGlossaryGlossary,
|
||||||
AddonModGlossaryNewEntry,
|
|
||||||
AddonModGlossaryNewEntryWithFiles,
|
|
||||||
AddonModGlossaryProvider,
|
AddonModGlossaryProvider,
|
||||||
} from '../../services/glossary';
|
} from '../../services/glossary';
|
||||||
import { AddonModGlossaryHelper } from '../../services/glossary-helper';
|
import { AddonModGlossaryHelper } from '../../services/glossary-helper';
|
||||||
|
@ -50,7 +48,7 @@ import { AddonModGlossaryOffline } from '../../services/glossary-offline';
|
||||||
selector: 'page-addon-mod-glossary-edit',
|
selector: 'page-addon-mod-glossary-edit',
|
||||||
templateUrl: 'edit.html',
|
templateUrl: 'edit.html',
|
||||||
})
|
})
|
||||||
export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
export class AddonModGlossaryEditPage implements OnInit, CanLeave {
|
||||||
|
|
||||||
@ViewChild('editFormEl') formElement?: ElementRef;
|
@ViewChild('editFormEl') formElement?: ElementRef;
|
||||||
|
|
||||||
|
@ -59,32 +57,28 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
courseId!: number;
|
courseId!: number;
|
||||||
loaded = false;
|
loaded = false;
|
||||||
glossary?: AddonModGlossaryGlossary;
|
glossary?: AddonModGlossaryGlossary;
|
||||||
attachments: FileEntry[] = [];
|
|
||||||
definitionControl = new FormControl();
|
definitionControl = new FormControl();
|
||||||
categories: AddonModGlossaryCategory[] = [];
|
categories: AddonModGlossaryCategory[] = [];
|
||||||
|
showAliases = true;
|
||||||
editorExtraParams: Record<string, unknown> = {};
|
editorExtraParams: Record<string, unknown> = {};
|
||||||
entry: AddonModGlossaryNewEntry = {
|
handler!: AddonModGlossaryFormHandler;
|
||||||
|
data: AddonModGlossaryFormData = {
|
||||||
concept: '',
|
concept: '',
|
||||||
definition: '',
|
definition: '',
|
||||||
timecreated: 0,
|
timecreated: 0,
|
||||||
};
|
attachments: [],
|
||||||
|
categories: [],
|
||||||
entries?: AddonModGlossaryEditEntriesSwipeManager;
|
|
||||||
|
|
||||||
options = {
|
|
||||||
categories: <string[]> [],
|
|
||||||
aliases: '',
|
aliases: '',
|
||||||
usedynalink: false,
|
usedynalink: false,
|
||||||
casesensitive: false,
|
casesensitive: false,
|
||||||
fullmatch: false,
|
fullmatch: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected timecreated!: number;
|
originalData?: AddonModGlossaryFormData;
|
||||||
protected concept = '';
|
|
||||||
protected syncId?: string;
|
protected syncId?: string;
|
||||||
protected syncObserver?: CoreEventObserver;
|
protected syncObserver?: CoreEventObserver;
|
||||||
protected isDestroyed = false;
|
protected isDestroyed = false;
|
||||||
protected originalData?: AddonModGlossaryNewEntryWithFiles;
|
|
||||||
protected saved = false;
|
protected saved = false;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
|
constructor(protected route: ActivatedRoute, @Optional() protected splitView: CoreSplitViewComponent) {}
|
||||||
|
@ -94,22 +88,21 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const routeData = this.route.snapshot.data;
|
const entrySlug = CoreNavigator.getRouteParam<string>('entrySlug');
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.timecreated = CoreNavigator.getRequiredRouteNumberParam('timecreated');
|
|
||||||
this.concept = CoreNavigator.getRouteParam<string>('concept') || '';
|
|
||||||
this.editorExtraParams.timecreated = this.timecreated;
|
|
||||||
|
|
||||||
if (this.timecreated !== 0 && (routeData.swipeEnabled ?? true)) {
|
if (entrySlug?.startsWith('new-')) {
|
||||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
const timecreated = Number(entrySlug.slice(4));
|
||||||
AddonModGlossaryEntriesSource,
|
this.editorExtraParams.timecreated = timecreated;
|
||||||
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
|
this.handler = new AddonModGlossaryOfflineFormHandler(this, timecreated);
|
||||||
);
|
} else if (entrySlug) {
|
||||||
|
const { entry } = await AddonModGlossary.getEntry(Number(entrySlug));
|
||||||
|
|
||||||
this.entries = new AddonModGlossaryEditEntriesSwipeManager(source);
|
this.editorExtraParams.timecreated = entry.timecreated;
|
||||||
|
this.handler = new AddonModGlossaryOnlineFormHandler(this, entry);
|
||||||
await this.entries.start();
|
} else {
|
||||||
|
this.handler = new AddonModGlossaryNewFormHandler(this);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
@ -122,13 +115,6 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.entries?.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch required data.
|
* Fetch required data.
|
||||||
*
|
*
|
||||||
|
@ -138,13 +124,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
try {
|
try {
|
||||||
this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId);
|
this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId);
|
||||||
|
|
||||||
if (this.timecreated > 0) {
|
await this.handler.loadData(this.glossary);
|
||||||
await this.loadOfflineData();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.categories = await AddonModGlossary.getAllCategories(this.glossary.id, {
|
|
||||||
cmId: this.cmId,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -154,64 +134,21 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load offline data when editing an offline entry.
|
|
||||||
*
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
|
||||||
protected async loadOfflineData(): Promise<void> {
|
|
||||||
if (!this.glossary) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = await AddonModGlossaryOffline.getNewEntry(this.glossary.id, this.concept, this.timecreated);
|
|
||||||
|
|
||||||
this.entry.concept = entry.concept || '';
|
|
||||||
this.entry.definition = entry.definition || '';
|
|
||||||
this.entry.timecreated = entry.timecreated;
|
|
||||||
|
|
||||||
this.originalData = {
|
|
||||||
concept: this.entry.concept,
|
|
||||||
definition: this.entry.definition,
|
|
||||||
files: [],
|
|
||||||
timecreated: entry.timecreated,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (entry.options) {
|
|
||||||
this.options.categories = (entry.options.categories && (<string> entry.options.categories).split(',')) || [];
|
|
||||||
this.options.aliases = <string> entry.options.aliases || '';
|
|
||||||
this.options.usedynalink = !!entry.options.usedynalink;
|
|
||||||
if (this.options.usedynalink) {
|
|
||||||
this.options.casesensitive = !!entry.options.casesensitive;
|
|
||||||
this.options.fullmatch = !!entry.options.fullmatch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat offline attachments if any.
|
|
||||||
if (entry.attachments?.offline) {
|
|
||||||
this.attachments = await AddonModGlossaryHelper.getStoredFiles(this.glossary.id, entry.concept, entry.timecreated);
|
|
||||||
|
|
||||||
this.originalData.files = this.attachments.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.definitionControl.setValue(this.entry.definition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the form data.
|
* Reset the form data.
|
||||||
*/
|
*/
|
||||||
protected resetForm(): void {
|
protected resetForm(): void {
|
||||||
this.entry.concept = '';
|
|
||||||
this.entry.definition = '';
|
|
||||||
this.entry.timecreated = 0;
|
|
||||||
this.originalData = undefined;
|
this.originalData = undefined;
|
||||||
|
|
||||||
this.options.categories = [];
|
this.data.concept = '';
|
||||||
this.options.aliases = '';
|
this.data.definition = '';
|
||||||
this.options.usedynalink = false;
|
this.data.timecreated = 0;
|
||||||
this.options.casesensitive = false;
|
this.data.categories = [];
|
||||||
this.options.fullmatch = false;
|
this.data.aliases = '';
|
||||||
this.attachments.length = 0; // Empty the array.
|
this.data.usedynalink = false;
|
||||||
|
this.data.casesensitive = false;
|
||||||
|
this.data.fullmatch = false;
|
||||||
|
this.data.attachments.length = 0; // Empty the array.
|
||||||
|
|
||||||
this.definitionControl.setValue('');
|
this.definitionControl.setValue('');
|
||||||
}
|
}
|
||||||
|
@ -222,7 +159,7 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
* @param text The new text.
|
* @param text The new text.
|
||||||
*/
|
*/
|
||||||
onDefinitionChange(text: string): void {
|
onDefinitionChange(text: string): void {
|
||||||
this.entry.definition = text;
|
this.data.definition = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -235,13 +172,13 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AddonModGlossaryHelper.hasEntryDataChanged(this.entry, this.attachments, this.originalData)) {
|
if (this.hasDataChanged()) {
|
||||||
// Show confirmation if some data has been modified.
|
// Show confirmation if some data has been modified.
|
||||||
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
|
await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the local files from the tmp folder.
|
// Delete the local files from the tmp folder.
|
||||||
CoreFileUploader.clearTmpFiles(this.attachments);
|
CoreFileUploader.clearTmpFiles(this.data.attachments);
|
||||||
|
|
||||||
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
|
CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId());
|
||||||
|
|
||||||
|
@ -252,114 +189,26 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
* Save the entry.
|
* Save the entry.
|
||||||
*/
|
*/
|
||||||
async save(): Promise<void> {
|
async save(): Promise<void> {
|
||||||
let definition = this.entry.definition;
|
if (!this.data.concept || !this.data.definition) {
|
||||||
let entryId: number | undefined;
|
|
||||||
const timecreated = this.entry.timecreated || Date.now();
|
|
||||||
|
|
||||||
if (!this.entry.concept || !definition) {
|
|
||||||
CoreDomUtils.showErrorModal('addon.mod_glossary.fillfields', true);
|
CoreDomUtils.showErrorModal('addon.mod_glossary.fillfields', true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.glossary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||||
definition = CoreTextUtils.formatHtmlLines(definition);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.glossary) {
|
const savedOnline = await this.handler.save(this.glossary);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload attachments first if any.
|
this.saved = true;
|
||||||
const { saveOffline, attachmentsResult } = await this.uploadAttachments(timecreated);
|
|
||||||
|
|
||||||
const options: Record<string, AddonModGlossaryEntryOption> = {
|
CoreForms.triggerFormSubmittedEvent(this.formElement, savedOnline, CoreSites.getCurrentSiteId());
|
||||||
aliases: this.options.aliases,
|
|
||||||
categories: this.options.categories.join(','),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.glossary.usedynalink) {
|
this.goBack();
|
||||||
options.usedynalink = this.options.usedynalink ? 1 : 0;
|
|
||||||
if (this.options.usedynalink) {
|
|
||||||
options.casesensitive = this.options.casesensitive ? 1 : 0;
|
|
||||||
options.fullmatch = this.options.fullmatch ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (saveOffline) {
|
|
||||||
if (this.entry && !this.glossary.allowduplicatedentries) {
|
|
||||||
// Check if the entry is duplicated in online or offline mode.
|
|
||||||
const isUsed = await AddonModGlossary.isConceptUsed(this.glossary.id, this.entry.concept, {
|
|
||||||
timeCreated: this.entry.timecreated,
|
|
||||||
cmId: this.cmId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isUsed) {
|
|
||||||
// There's a entry with same name, reject with error message.
|
|
||||||
throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save entry in offline.
|
|
||||||
await AddonModGlossaryOffline.addNewEntry(
|
|
||||||
this.glossary.id,
|
|
||||||
this.entry.concept,
|
|
||||||
definition,
|
|
||||||
this.courseId,
|
|
||||||
options,
|
|
||||||
<CoreFileUploaderStoreFilesResult> attachmentsResult,
|
|
||||||
timecreated,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
this.entry,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Try to send it to server.
|
|
||||||
// Don't allow offline if there are attachments since they were uploaded fine.
|
|
||||||
await AddonModGlossary.addEntry(
|
|
||||||
this.glossary.id,
|
|
||||||
this.entry.concept,
|
|
||||||
definition,
|
|
||||||
this.courseId,
|
|
||||||
options,
|
|
||||||
attachmentsResult,
|
|
||||||
{
|
|
||||||
timeCreated: timecreated,
|
|
||||||
discardEntry: this.entry,
|
|
||||||
allowOffline: !this.attachments.length,
|
|
||||||
checkDuplicates: !this.glossary.allowduplicatedentries,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the local files from the tmp folder.
|
|
||||||
CoreFileUploader.clearTmpFiles(this.attachments);
|
|
||||||
|
|
||||||
if (entryId) {
|
|
||||||
// Data sent to server, delete stored files (if any).
|
|
||||||
AddonModGlossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated);
|
|
||||||
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
|
|
||||||
}
|
|
||||||
|
|
||||||
CoreEvents.trigger(AddonModGlossaryProvider.ADD_ENTRY_EVENT, {
|
|
||||||
glossaryId: this.glossary.id,
|
|
||||||
entryId: entryId,
|
|
||||||
}, CoreSites.getCurrentSiteId());
|
|
||||||
|
|
||||||
CoreForms.triggerFormSubmittedEvent(this.formElement, !!entryId, CoreSites.getCurrentSiteId());
|
|
||||||
|
|
||||||
if (this.splitView?.outletActivated) {
|
|
||||||
if (this.timecreated > 0) {
|
|
||||||
// Reload the data.
|
|
||||||
await this.loadOfflineData();
|
|
||||||
} else {
|
|
||||||
// Empty form.
|
|
||||||
this.resetForm();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.saved = true;
|
|
||||||
CoreNavigator.back();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.cannoteditentry', true);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -368,49 +217,21 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload entry attachments if any.
|
* Check if the form data has changed.
|
||||||
*
|
*
|
||||||
* @param timecreated Entry's timecreated.
|
* @returns True if data has changed, false otherwise.
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
*/
|
||||||
protected async uploadAttachments(
|
protected hasDataChanged(): boolean {
|
||||||
timecreated: number,
|
if (!this.originalData || this.originalData.concept === undefined) {
|
||||||
): Promise<{saveOffline: boolean; attachmentsResult?: number | CoreFileUploaderStoreFilesResult}> {
|
// There is no original data.
|
||||||
if (!this.attachments.length || !this.glossary) {
|
return !!(this.data.definition || this.data.concept || this.data.attachments.length > 0);
|
||||||
return {
|
|
||||||
saveOffline: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (this.originalData.definition != this.data.definition || this.originalData.concept != this.data.concept) {
|
||||||
const attachmentsResult = await CoreFileUploader.uploadOrReuploadFiles(
|
return true;
|
||||||
this.attachments,
|
|
||||||
AddonModGlossaryProvider.COMPONENT,
|
|
||||||
this.glossary.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
saveOffline: false,
|
|
||||||
attachmentsResult,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (CoreUtils.isWebServiceError(error)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cannot upload them in online, save them in offline.
|
|
||||||
const attachmentsResult = await AddonModGlossaryHelper.storeFiles(
|
|
||||||
this.glossary.id,
|
|
||||||
this.entry.concept,
|
|
||||||
timecreated,
|
|
||||||
this.attachments,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
saveOffline: true,
|
|
||||||
attachmentsResult,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return CoreFileUploader.areFileListDifferent(this.data.attachments, this.originalData.attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -427,15 +248,463 @@ export class AddonModGlossaryEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to manage swiping within a collection of glossary entries.
|
* Helper to manage form data.
|
||||||
*/
|
*/
|
||||||
class AddonModGlossaryEditEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager {
|
abstract class AddonModGlossaryFormHandler {
|
||||||
|
|
||||||
|
constructor(protected page: AddonModGlossaryEditPage) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load form data.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
*/
|
||||||
|
abstract loadData(glossary: AddonModGlossaryGlossary): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save form data.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
* @returns Whether the form was saved online.
|
||||||
|
*/
|
||||||
|
abstract save(glossary: AddonModGlossaryGlossary): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load form categories.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
*/
|
||||||
|
protected async loadCategories(glossary: AddonModGlossaryGlossary): Promise<void> {
|
||||||
|
this.page.categories = await AddonModGlossary.getAllCategories(glossary.id, {
|
||||||
|
cmId: this.page.cmId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload attachments online.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
* @returns Uploaded attachments item id.
|
||||||
|
*/
|
||||||
|
protected async uploadAttachments(glossary: AddonModGlossaryGlossary): Promise<number> {
|
||||||
|
const data = this.page.data;
|
||||||
|
const itemId = await CoreFileUploader.uploadOrReuploadFiles(
|
||||||
|
data.attachments,
|
||||||
|
AddonModGlossaryProvider.COMPONENT,
|
||||||
|
glossary.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store attachments offline.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
* @param timecreated Entry time created.
|
||||||
|
* @returns Storage result.
|
||||||
|
*/
|
||||||
|
protected async storeAttachments(
|
||||||
|
glossary: AddonModGlossaryGlossary,
|
||||||
|
timecreated: number,
|
||||||
|
): Promise<CoreFileUploaderStoreFilesResult> {
|
||||||
|
const data = this.page.data;
|
||||||
|
const result = await AddonModGlossaryHelper.storeFiles(
|
||||||
|
glossary.id,
|
||||||
|
data.concept,
|
||||||
|
timecreated,
|
||||||
|
data.attachments,
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure that the new entry won't create any duplicates.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
*/
|
||||||
|
protected async checkDuplicates(glossary: AddonModGlossaryGlossary): Promise<void> {
|
||||||
|
if (glossary.allowduplicatedentries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = this.page.data;
|
||||||
|
const isUsed = await AddonModGlossary.isConceptUsed(glossary.id, data.concept, {
|
||||||
|
timeCreated: data.timecreated,
|
||||||
|
cmId: this.page.cmId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isUsed) {
|
||||||
|
// There's a entry with same name, reject with error message.
|
||||||
|
throw new CoreError(Translate.instant('addon.mod_glossary.errconceptalreadyexists'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get additional options to save an entry.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
* @returns Options.
|
||||||
|
*/
|
||||||
|
protected getSaveOptions(glossary: AddonModGlossaryGlossary): Record<string, AddonModGlossaryEntryOption> {
|
||||||
|
const data = this.page.data;
|
||||||
|
const options: Record<string, AddonModGlossaryEntryOption> = {};
|
||||||
|
|
||||||
|
if (this.page.showAliases) {
|
||||||
|
options.aliases = data.aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.page.categories.length > 0) {
|
||||||
|
options.categories = data.categories.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glossary.usedynalink) {
|
||||||
|
options.usedynalink = data.usedynalink ? 1 : 0;
|
||||||
|
|
||||||
|
if (data.usedynalink) {
|
||||||
|
options.casesensitive = data.casesensitive ? 1 : 0;
|
||||||
|
options.fullmatch = data.fullmatch ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage the form data for an offline entry.
|
||||||
|
*/
|
||||||
|
class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler {
|
||||||
|
|
||||||
|
private timecreated: number;
|
||||||
|
|
||||||
|
constructor(page: AddonModGlossaryEditPage, timecreated: number) {
|
||||||
|
super(page);
|
||||||
|
|
||||||
|
this.timecreated = timecreated;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
async loadData(glossary: AddonModGlossaryGlossary): Promise<void> {
|
||||||
return `${this.getSource().GLOSSARY_PATH_PREFIX}edit/${route.params.timecreated}`;
|
const data = this.page.data;
|
||||||
|
const entry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, this.timecreated);
|
||||||
|
|
||||||
|
data.concept = entry.concept || '';
|
||||||
|
data.definition = entry.definition || '';
|
||||||
|
data.timecreated = entry.timecreated;
|
||||||
|
|
||||||
|
if (entry.options) {
|
||||||
|
data.categories = ((entry.options.categories as string)?.split(',') ?? []).map(id => Number(id));
|
||||||
|
data.aliases = entry.options.aliases as string ?? '';
|
||||||
|
data.usedynalink = !!entry.options.usedynalink;
|
||||||
|
|
||||||
|
if (data.usedynalink) {
|
||||||
|
data.casesensitive = !!entry.options.casesensitive;
|
||||||
|
data.fullmatch = !!entry.options.fullmatch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat offline attachments if any.
|
||||||
|
if (entry.attachments?.offline) {
|
||||||
|
data.attachments = await AddonModGlossaryHelper.getStoredFiles(glossary.id, entry.concept, entry.timecreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.page.originalData = {
|
||||||
|
concept: data.concept,
|
||||||
|
definition: data.definition,
|
||||||
|
attachments: data.attachments.slice(),
|
||||||
|
timecreated: data.timecreated,
|
||||||
|
categories: data.categories.slice(),
|
||||||
|
aliases: data.aliases,
|
||||||
|
usedynalink: data.usedynalink,
|
||||||
|
casesensitive: data.casesensitive,
|
||||||
|
fullmatch: data.fullmatch,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.page.definitionControl.setValue(data.definition);
|
||||||
|
|
||||||
|
await this.loadCategories(glossary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async save(glossary: AddonModGlossaryGlossary): Promise<boolean> {
|
||||||
|
const originalData = this.page.data;
|
||||||
|
const data = this.page.data;
|
||||||
|
|
||||||
|
// Upload attachments first if any.
|
||||||
|
let offlineAttachments: CoreFileUploaderStoreFilesResult | undefined = undefined;
|
||||||
|
|
||||||
|
if (data.attachments.length) {
|
||||||
|
offlineAttachments = await this.storeAttachments(glossary, data.timecreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalData.concept !== data.concept) {
|
||||||
|
await AddonModGlossaryHelper.deleteStoredFiles(glossary.id, originalData.concept, data.timecreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save entry data.
|
||||||
|
await this.updateOfflineEntry(glossary, offlineAttachments);
|
||||||
|
|
||||||
|
// Delete the local files from the tmp folder.
|
||||||
|
CoreFileUploader.clearTmpFiles(data.attachments);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an offline entry.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
* @param uploadedAttachments Uploaded attachments.
|
||||||
|
*/
|
||||||
|
protected async updateOfflineEntry(
|
||||||
|
glossary: AddonModGlossaryGlossary,
|
||||||
|
uploadedAttachments?: CoreFileUploaderStoreFilesResult,
|
||||||
|
): Promise<void> {
|
||||||
|
const originalData = this.page.originalData;
|
||||||
|
const data = this.page.data;
|
||||||
|
const options = this.getSaveOptions(glossary);
|
||||||
|
const definition = CoreTextUtils.formatHtmlLines(data.definition);
|
||||||
|
|
||||||
|
if (!originalData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.checkDuplicates(glossary);
|
||||||
|
await AddonModGlossaryOffline.updateOfflineEntry(
|
||||||
|
{
|
||||||
|
glossaryid: glossary.id,
|
||||||
|
courseid: this.page.courseId,
|
||||||
|
concept: originalData.concept,
|
||||||
|
timecreated: originalData.timecreated,
|
||||||
|
},
|
||||||
|
data.concept,
|
||||||
|
definition,
|
||||||
|
options,
|
||||||
|
uploadedAttachments,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage the form data for creating a new entry.
|
||||||
|
*/
|
||||||
|
class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async loadData(glossary: AddonModGlossaryGlossary): Promise<void> {
|
||||||
|
await this.loadCategories(glossary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async save(glossary: AddonModGlossaryGlossary): Promise<boolean> {
|
||||||
|
const data = this.page.data;
|
||||||
|
const timecreated = Date.now();
|
||||||
|
|
||||||
|
// Upload attachments first if any.
|
||||||
|
let onlineAttachments: number | undefined = undefined;
|
||||||
|
let offlineAttachments: CoreFileUploaderStoreFilesResult | undefined = undefined;
|
||||||
|
|
||||||
|
if (data.attachments.length) {
|
||||||
|
try {
|
||||||
|
onlineAttachments = await this.uploadAttachments(glossary);
|
||||||
|
} catch (error) {
|
||||||
|
if (CoreUtils.isWebServiceError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
offlineAttachments = await this.storeAttachments(glossary, timecreated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save entry data.
|
||||||
|
const entryId = offlineAttachments
|
||||||
|
? await this.createOfflineEntry(glossary, timecreated, offlineAttachments)
|
||||||
|
: await this.createOnlineEntry(glossary, timecreated, onlineAttachments, !data.attachments.length);
|
||||||
|
|
||||||
|
// Delete the local files from the tmp folder.
|
||||||
|
CoreFileUploader.clearTmpFiles(data.attachments);
|
||||||
|
|
||||||
|
if (entryId) {
|
||||||
|
// Data sent to server, delete stored files (if any).
|
||||||
|
AddonModGlossaryHelper.deleteStoredFiles(glossary.id, data.concept, timecreated);
|
||||||
|
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!entryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an offline entry.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
* @param timecreated Time created.
|
||||||
|
* @param uploadedAttachments Uploaded attachments.
|
||||||
|
*/
|
||||||
|
protected async createOfflineEntry(
|
||||||
|
glossary: AddonModGlossaryGlossary,
|
||||||
|
timecreated: number,
|
||||||
|
uploadedAttachments?: CoreFileUploaderStoreFilesResult,
|
||||||
|
): Promise<void> {
|
||||||
|
const data = this.page.data;
|
||||||
|
const options = this.getSaveOptions(glossary);
|
||||||
|
const definition = CoreTextUtils.formatHtmlLines(data.definition);
|
||||||
|
|
||||||
|
await this.checkDuplicates(glossary);
|
||||||
|
await AddonModGlossaryOffline.addOfflineEntry(
|
||||||
|
glossary.id,
|
||||||
|
data.concept,
|
||||||
|
definition,
|
||||||
|
this.page.courseId,
|
||||||
|
timecreated,
|
||||||
|
options,
|
||||||
|
uploadedAttachments,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an online entry.
|
||||||
|
*
|
||||||
|
* @param glossary Glossary.
|
||||||
|
* @param timecreated Time created.
|
||||||
|
* @param uploadedAttachmentsId Id of the uploaded attachments.
|
||||||
|
* @param allowOffline Allow falling back to creating the entry offline.
|
||||||
|
* @returns Entry id.
|
||||||
|
*/
|
||||||
|
protected async createOnlineEntry(
|
||||||
|
glossary: AddonModGlossaryGlossary,
|
||||||
|
timecreated: number,
|
||||||
|
uploadedAttachmentsId?: number,
|
||||||
|
allowOffline?: boolean,
|
||||||
|
): Promise<number | false> {
|
||||||
|
const data = this.page.data;
|
||||||
|
const options = this.getSaveOptions(glossary);
|
||||||
|
const definition = CoreTextUtils.formatHtmlLines(data.definition);
|
||||||
|
const entryId = await AddonModGlossary.addEntry(
|
||||||
|
glossary.id,
|
||||||
|
data.concept,
|
||||||
|
definition,
|
||||||
|
this.page.courseId,
|
||||||
|
options,
|
||||||
|
uploadedAttachmentsId,
|
||||||
|
{
|
||||||
|
timeCreated: timecreated,
|
||||||
|
allowOffline: allowOffline,
|
||||||
|
checkDuplicates: !glossary.allowduplicatedentries,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return entryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to manage the form data for an online entry.
|
||||||
|
*/
|
||||||
|
class AddonModGlossaryOnlineFormHandler extends AddonModGlossaryFormHandler {
|
||||||
|
|
||||||
|
private entry: AddonModGlossaryEntry;
|
||||||
|
|
||||||
|
constructor(page: AddonModGlossaryEditPage, entry: AddonModGlossaryEntry) {
|
||||||
|
super(page);
|
||||||
|
|
||||||
|
this.entry = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async loadData(): Promise<void> {
|
||||||
|
const data = this.page.data;
|
||||||
|
|
||||||
|
data.concept = this.entry.concept;
|
||||||
|
data.definition = this.entry.definition || '';
|
||||||
|
data.timecreated = this.entry.timecreated;
|
||||||
|
data.usedynalink = this.entry.usedynalink;
|
||||||
|
|
||||||
|
if (data.usedynalink) {
|
||||||
|
data.casesensitive = this.entry.casesensitive;
|
||||||
|
data.fullmatch = this.entry.fullmatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat offline attachments if any.
|
||||||
|
if (this.entry.attachments) {
|
||||||
|
data.attachments = this.entry.attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.page.originalData = {
|
||||||
|
concept: data.concept,
|
||||||
|
definition: data.definition,
|
||||||
|
attachments: data.attachments.slice(),
|
||||||
|
timecreated: data.timecreated,
|
||||||
|
categories: data.categories.slice(),
|
||||||
|
aliases: data.aliases,
|
||||||
|
usedynalink: data.usedynalink,
|
||||||
|
casesensitive: data.casesensitive,
|
||||||
|
fullmatch: data.fullmatch,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.page.definitionControl.setValue(data.definition);
|
||||||
|
this.page.showAliases = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async save(glossary: AddonModGlossaryGlossary): Promise<boolean> {
|
||||||
|
if (!CoreNetwork.isOnline()) {
|
||||||
|
throw new CoreNetworkError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = this.page.data;
|
||||||
|
const options = this.getSaveOptions(glossary);
|
||||||
|
const definition = CoreTextUtils.formatHtmlLines(data.definition);
|
||||||
|
|
||||||
|
// Upload attachments, if any.
|
||||||
|
let attachmentsId: number | undefined = undefined;
|
||||||
|
|
||||||
|
if (data.attachments.length) {
|
||||||
|
attachmentsId = await this.uploadAttachments(glossary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save entry data.
|
||||||
|
await AddonModGlossary.updateEntry(glossary.id, this.entry.id, data.concept, definition, options, attachmentsId);
|
||||||
|
|
||||||
|
// Delete the local files from the tmp folder.
|
||||||
|
CoreFileUploader.clearTmpFiles(data.attachments);
|
||||||
|
|
||||||
|
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'glossary' });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form data.
|
||||||
|
*/
|
||||||
|
type AddonModGlossaryFormData = {
|
||||||
|
concept: string;
|
||||||
|
definition: string;
|
||||||
|
timecreated: number;
|
||||||
|
attachments: CoreFileEntry[];
|
||||||
|
categories: number[];
|
||||||
|
aliases: string;
|
||||||
|
usedynalink: boolean;
|
||||||
|
casesensitive: boolean;
|
||||||
|
fullmatch: boolean;
|
||||||
|
};
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
|
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<ng-container *ngIf="entry && loaded">
|
<ng-container *ngIf="entry && loaded">
|
||||||
|
<ion-card *ngIf="offlineEntry" class="core-warning-card">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-label>{{ 'core.hasdatatosync' | translate: { $a: 'addon.mod_glossary.entry' | translate } }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
<ion-item class="ion-text-wrap" *ngIf="showAuthor">
|
||||||
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>
|
<core-user-avatar [user]="entry" slot="start"></core-user-avatar>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -26,9 +32,9 @@
|
||||||
[courseId]="courseId">
|
[courseId]="courseId">
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</h2>
|
</h2>
|
||||||
<p>{{ entry.userfullname }}</p>
|
<p *ngIf="onlineEntry">{{ onlineEntry.userfullname }}</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
<ion-note slot="end" *ngIf="showDate && onlineEntry">{{ onlineEntry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
|
<ion-item class="ion-text-wrap" *ngIf="!showAuthor">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -37,7 +43,7 @@
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</p>
|
</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
<ion-note slot="end" *ngIf="showDate">{{ entry.timemodified | coreDateDayOrTime }}</ion-note>
|
<ion-note slot="end" *ngIf="showDate && onlineEntry">{{ onlineEntry.timemodified | coreDateDayOrTime }}</ion-note>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap">
|
<ion-item class="ion-text-wrap">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -46,32 +52,53 @@
|
||||||
</core-format-text>
|
</core-format-text>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<div *ngIf="entry.attachment">
|
<ion-item *ngIf="canDelete || canEdit">
|
||||||
<core-file *ngFor="let file of entry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
<div slot="end">
|
||||||
|
<ion-button *ngIf="canDelete" fill="clear" (click)="deleteEntry()"
|
||||||
|
[attr.aria-label]="'addon.mod_glossary.deleteentry' | translate">
|
||||||
|
<ion-icon slot="icon-only" name="fas-trash" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button *ngIf="canEdit" fill="clear" (click)="editEntry()"
|
||||||
|
[attr.aria-label]="'addon.mod_glossary.editentry' | translate">
|
||||||
|
<ion-icon slot="icon-only" name="fas-pen" aria-hidden="true"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
<div *ngIf="onlineEntry && onlineEntry.attachment">
|
||||||
|
<core-file *ngFor="let file of onlineEntry.attachments" [file]="file" [component]="component" [componentId]="componentId">
|
||||||
</core-file>
|
</core-file>
|
||||||
</div>
|
</div>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="tagsEnabled && entry && entry.tags && entry.tags.length > 0">
|
<div *ngIf="offlineEntry && offlineEntry.attachments">
|
||||||
|
<core-file *ngFor="let file of offlineEntry.attachments.online" [file]="file" [component]="component"
|
||||||
|
[componentId]="componentId">
|
||||||
|
</core-file>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="offlineEntry && offlineEntryFiles">
|
||||||
|
<core-local-file *ngFor="let file of offlineEntryFiles" [file]="file">
|
||||||
|
</core-local-file>
|
||||||
|
</div>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="onlineEntry && tagsEnabled && entry && onlineEntry.tags && onlineEntry.tags.length > 0">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
<div slot="start">{{ 'core.tag.tags' | translate }}:</div>
|
||||||
<core-tag-list [tags]="entry.tags"></core-tag-list>
|
<core-tag-list [tags]="onlineEntry.tags"></core-tag-list>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item class="ion-text-wrap" *ngIf="!entry.approved">
|
<ion-item class="ion-text-wrap" *ngIf="onlineEntry && !onlineEntry.approved">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
|
<p><em>{{ 'addon.mod_glossary.entrypendingapproval' | translate }}</em></p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<core-comments *ngIf="glossary && glossary.allowcomments && entry && entry.id > 0 && commentsEnabled" contextLevel="module"
|
<core-comments *ngIf="glossary && glossary.allowcomments && onlineEntry && onlineEntry.id > 0 && commentsEnabled"
|
||||||
[instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="entry.id" area="glossary_entry"
|
contextLevel="module" [instanceId]="glossary.coursemodule" component="mod_glossary" [itemId]="onlineEntry.id"
|
||||||
[courseId]="glossary.course" [showItem]="true">
|
area="glossary_entry" [courseId]="glossary.course" [showItem]="true">
|
||||||
</core-comments>
|
</core-comments>
|
||||||
<core-rating-rate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
<core-rating-rate *ngIf="glossary && ratingInfo && onlineEntry" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||||
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [itemSetId]="0" [courseId]="glossary.course"
|
[instanceId]="glossary.coursemodule" [itemId]="onlineEntry.id" [itemSetId]="0" [courseId]="glossary.course"
|
||||||
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
|
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale" [userId]="entry.userid" (onUpdate)="ratingUpdated()">
|
||||||
</core-rating-rate>
|
</core-rating-rate>
|
||||||
<core-rating-aggregate *ngIf="glossary && ratingInfo" [ratingInfo]="ratingInfo" contextLevel="module"
|
<core-rating-aggregate *ngIf="glossary && ratingInfo && onlineEntry" [ratingInfo]="ratingInfo" contextLevel="module"
|
||||||
[instanceId]="glossary.coursemodule" [itemId]="entry.id" [courseId]="glossary.course" [aggregateMethod]="glossary.assessed"
|
[instanceId]="glossary.coursemodule" [itemId]="onlineEntry.id" [courseId]="glossary.course"
|
||||||
[scaleId]="glossary.scale">
|
[aggregateMethod]="glossary.assessed" [scaleId]="glossary.scale">
|
||||||
</core-rating-aggregate>
|
</core-rating-aggregate>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
|
@ -12,24 +12,32 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { AddonModGlossaryHelper } from '@addons/mod/glossary/services/glossary-helper';
|
||||||
|
import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from '@addons/mod/glossary/services/glossary-offline';
|
||||||
|
import { Component, OnDestroy, OnInit, Optional, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||||
|
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
||||||
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
|
||||||
import { CoreComments } from '@features/comments/services/comments';
|
import { CoreComments } from '@features/comments/services/comments';
|
||||||
import { CoreRatingInfo } from '@features/rating/services/rating';
|
import { CoreRatingInfo } from '@features/rating/services/rating';
|
||||||
import { CoreTag } from '@features/tag/services/tag';
|
import { CoreTag } from '@features/tag/services/tag';
|
||||||
|
import { FileEntry } from '@ionic-native/file/ngx';
|
||||||
import { IonRefresher } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreNetwork } from '@services/network';
|
||||||
|
import { CoreDomUtils, ToastDuration } from '@services/utils/dom';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { AddonModGlossaryEntriesSource } from '../../classes/glossary-entries-source';
|
import { Translate } from '@singletons';
|
||||||
import { AddonModGlossaryEntriesSwipeManager } from '../../classes/glossary-entries-swipe-manager';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { AddonModGlossaryEntriesSource, AddonModGlossaryEntryItem } from '../../classes/glossary-entries-source';
|
||||||
import {
|
import {
|
||||||
AddonModGlossary,
|
AddonModGlossary,
|
||||||
AddonModGlossaryEntry,
|
AddonModGlossaryEntry,
|
||||||
AddonModGlossaryGlossary,
|
AddonModGlossaryGlossary,
|
||||||
AddonModGlossaryProvider,
|
AddonModGlossaryProvider,
|
||||||
|
GLOSSARY_ENTRY_UPDATED,
|
||||||
} from '../../services/glossary';
|
} from '../../services/glossary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,62 +53,90 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
component = AddonModGlossaryProvider.COMPONENT;
|
component = AddonModGlossaryProvider.COMPONENT;
|
||||||
componentId?: number;
|
componentId?: number;
|
||||||
entry?: AddonModGlossaryEntry;
|
onlineEntry?: AddonModGlossaryEntry;
|
||||||
entries?: AddonModGlossaryEntryEntriesSwipeManager;
|
offlineEntry?: AddonModGlossaryOfflineEntry;
|
||||||
|
offlineEntryFiles?: FileEntry[];
|
||||||
|
entries!: AddonModGlossaryEntryEntriesSwipeManager;
|
||||||
glossary?: AddonModGlossaryGlossary;
|
glossary?: AddonModGlossaryGlossary;
|
||||||
|
entryUpdatedObserver?: CoreEventObserver;
|
||||||
loaded = false;
|
loaded = false;
|
||||||
showAuthor = false;
|
showAuthor = false;
|
||||||
showDate = false;
|
showDate = false;
|
||||||
ratingInfo?: CoreRatingInfo;
|
ratingInfo?: CoreRatingInfo;
|
||||||
tagsEnabled = false;
|
tagsEnabled = false;
|
||||||
|
canEdit = false;
|
||||||
|
canDelete = false;
|
||||||
commentsEnabled = false;
|
commentsEnabled = false;
|
||||||
courseId!: number;
|
courseId!: number;
|
||||||
cmId?: number;
|
cmId!: number;
|
||||||
|
|
||||||
protected entryId!: number;
|
constructor(@Optional() protected splitView: CoreSplitViewComponent, protected route: ActivatedRoute) {}
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute) {}
|
get entry(): AddonModGlossaryEntry | AddonModGlossaryOfflineEntry | undefined {
|
||||||
|
return this.onlineEntry ?? this.offlineEntry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
|
let onlineEntryId: number | null = null;
|
||||||
|
let offlineEntryTimeCreated: number | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const routeData = this.route.snapshot.data;
|
|
||||||
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
|
||||||
this.entryId = CoreNavigator.getRequiredRouteNumberParam('entryId');
|
|
||||||
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
|
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
|
||||||
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
||||||
|
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
||||||
|
|
||||||
if (routeData.swipeEnabled ?? true) {
|
const entrySlug = CoreNavigator.getRequiredRouteParam<string>('entrySlug');
|
||||||
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
|
const routeData = this.route.snapshot.data;
|
||||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||||
AddonModGlossaryEntriesSource,
|
AddonModGlossaryEntriesSource,
|
||||||
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
|
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
|
||||||
);
|
);
|
||||||
|
|
||||||
this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
|
this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
|
||||||
|
|
||||||
await this.entries.start();
|
await this.entries.start();
|
||||||
|
|
||||||
|
if (entrySlug.startsWith('new-')) {
|
||||||
|
offlineEntryTimeCreated = Number(entrySlug.slice(4));
|
||||||
} else {
|
} else {
|
||||||
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
|
onlineEntryId = Number(entrySlug);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModal(error);
|
CoreDomUtils.showErrorModal(error);
|
||||||
|
|
||||||
CoreNavigator.back();
|
CoreNavigator.back();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
this.entryUpdatedObserver = CoreEvents.on(GLOSSARY_ENTRY_UPDATED, data => {
|
||||||
await this.fetchEntry();
|
if (data.glossaryId !== this.glossary?.id) {
|
||||||
|
|
||||||
if (!this.glossary || !this.componentId) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(this.entryId, this.componentId, this.glossary.name));
|
if (
|
||||||
|
(this.onlineEntry && this.onlineEntry.id === data.entryId) ||
|
||||||
|
(this.offlineEntry && this.offlineEntry.timecreated === data.timecreated)
|
||||||
|
) {
|
||||||
|
this.doRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (onlineEntryId) {
|
||||||
|
await this.loadOnlineEntry(onlineEntryId);
|
||||||
|
|
||||||
|
if (!this.glossary || !this.componentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUtils.ignoreErrors(AddonModGlossary.logEntryView(onlineEntryId, this.componentId, this.glossary?.name));
|
||||||
|
} else if (offlineEntryTimeCreated) {
|
||||||
|
await this.loadOfflineEntry(offlineEntryTimeCreated);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
|
@ -110,7 +146,66 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.entries?.destroy();
|
this.entries.destroy();
|
||||||
|
this.entryUpdatedObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit entry.
|
||||||
|
*/
|
||||||
|
async editEntry(): Promise<void> {
|
||||||
|
await CoreNavigator.navigate('./edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete entry.
|
||||||
|
*/
|
||||||
|
async deleteEntry(): Promise<void> {
|
||||||
|
const glossaryId = this.glossary?.id;
|
||||||
|
const cancelled = await CoreUtils.promiseFails(
|
||||||
|
CoreDomUtils.showConfirm(Translate.instant('addon.mod_glossary.areyousuredelete')),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!glossaryId || cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = await CoreDomUtils.showModalLoading();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.onlineEntry) {
|
||||||
|
const entryId = this.onlineEntry.id;
|
||||||
|
|
||||||
|
await AddonModGlossary.deleteEntry(glossaryId, entryId);
|
||||||
|
await Promise.all([
|
||||||
|
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(entryId)),
|
||||||
|
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByLetter(glossaryId)),
|
||||||
|
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByAuthor(glossaryId)),
|
||||||
|
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByCategory(glossaryId)),
|
||||||
|
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'CREATION')),
|
||||||
|
CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntriesByDate(glossaryId, 'UPDATE')),
|
||||||
|
CoreUtils.ignoreErrors(this.entries.getSource().invalidateCache(false)),
|
||||||
|
]);
|
||||||
|
} else if (this.offlineEntry) {
|
||||||
|
const concept = this.offlineEntry.concept;
|
||||||
|
const timecreated = this.offlineEntry.timecreated;
|
||||||
|
|
||||||
|
await AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, timecreated);
|
||||||
|
await AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timecreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreDomUtils.showToast('addon.mod_glossary.entrydeleted', true, ToastDuration.LONG);
|
||||||
|
|
||||||
|
if (this.splitView?.outletActivated) {
|
||||||
|
await CoreNavigator.navigate('../');
|
||||||
|
} else {
|
||||||
|
await CoreNavigator.back();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errordeleting', true);
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,65 +215,110 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async doRefresh(refresher?: IonRefresher): Promise<void> {
|
async doRefresh(refresher?: IonRefresher): Promise<void> {
|
||||||
if (this.glossary?.allowcomments && this.entry && this.entry.id > 0 && this.commentsEnabled && this.comments) {
|
if (this.onlineEntry && this.glossary?.allowcomments && this.onlineEntry.id > 0 && this.commentsEnabled && this.comments) {
|
||||||
// Refresh comments. Don't add it to promises because we don't want the comments fetch to block the entry fetch.
|
// Refresh comments asynchronously (without blocking the current promise).
|
||||||
CoreUtils.ignoreErrors(this.comments.doRefresh());
|
CoreUtils.ignoreErrors(this.comments.doRefresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(this.entryId));
|
if (this.onlineEntry) {
|
||||||
|
await CoreUtils.ignoreErrors(AddonModGlossary.invalidateEntry(this.onlineEntry.id));
|
||||||
|
await this.loadOnlineEntry(this.onlineEntry.id);
|
||||||
|
} else if (this.offlineEntry) {
|
||||||
|
const entrySlug = CoreNavigator.getRequiredRouteParam<string>('entrySlug');
|
||||||
|
const timecreated = Number(entrySlug.slice(4));
|
||||||
|
|
||||||
await this.fetchEntry();
|
await this.loadOfflineEntry(timecreated);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
refresher?.complete();
|
refresher?.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience function to get the glossary entry.
|
* Load online entry data.
|
||||||
*
|
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
*/
|
||||||
protected async fetchEntry(): Promise<void> {
|
protected async loadOnlineEntry(entryId: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await AddonModGlossary.getEntry(this.entryId);
|
const result = await AddonModGlossary.getEntry(entryId);
|
||||||
|
const canDeleteEntries = CoreNetwork.isOnline() && await AddonModGlossary.canDeleteEntries();
|
||||||
|
const canUpdateEntries = CoreNetwork.isOnline() && await AddonModGlossary.canUpdateEntries();
|
||||||
|
|
||||||
this.entry = result.entry;
|
this.onlineEntry = result.entry;
|
||||||
this.ratingInfo = result.ratinginfo;
|
this.ratingInfo = result.ratinginfo;
|
||||||
|
this.canDelete = canDeleteEntries && !!result.permissions?.candelete;
|
||||||
|
this.canEdit = canUpdateEntries && !!result.permissions?.canupdate;
|
||||||
|
|
||||||
if (this.glossary) {
|
await this.loadGlossary();
|
||||||
// Glossary already loaded, nothing else to load.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the glossary.
|
|
||||||
this.glossary = await AddonModGlossary.getGlossaryById(this.courseId, this.entry.glossaryid);
|
|
||||||
this.componentId = this.glossary.coursemodule;
|
|
||||||
|
|
||||||
switch (this.glossary.displayformat) {
|
|
||||||
case 'fullwithauthor':
|
|
||||||
case 'encyclopedia':
|
|
||||||
this.showAuthor = true;
|
|
||||||
this.showDate = true;
|
|
||||||
break;
|
|
||||||
case 'fullwithoutauthor':
|
|
||||||
this.showAuthor = false;
|
|
||||||
this.showDate = true;
|
|
||||||
break;
|
|
||||||
default: // Default, and faq, simple, entrylist, continuous.
|
|
||||||
this.showAuthor = false;
|
|
||||||
this.showDate = false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load offline entry data.
|
||||||
|
*
|
||||||
|
* @param timecreated Entry Timecreated.
|
||||||
|
*/
|
||||||
|
protected async loadOfflineEntry(timecreated: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const glossary = await this.loadGlossary();
|
||||||
|
|
||||||
|
this.offlineEntry = await AddonModGlossaryOffline.getOfflineEntry(glossary.id, timecreated);
|
||||||
|
this.offlineEntryFiles = this.offlineEntry.attachments && this.offlineEntry.attachments.offline > 0
|
||||||
|
? await AddonModGlossaryHelper.getStoredFiles(
|
||||||
|
glossary.id,
|
||||||
|
this.offlineEntry.concept,
|
||||||
|
timecreated,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
this.canEdit = true;
|
||||||
|
this.canDelete = true;
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load glossary data.
|
||||||
|
*
|
||||||
|
* @returns Glossary.
|
||||||
|
*/
|
||||||
|
protected async loadGlossary(): Promise<AddonModGlossaryGlossary> {
|
||||||
|
if (this.glossary) {
|
||||||
|
return this.glossary;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.glossary = await AddonModGlossary.getGlossary(this.courseId, this.cmId);
|
||||||
|
this.componentId = this.glossary.coursemodule;
|
||||||
|
|
||||||
|
switch (this.glossary.displayformat) {
|
||||||
|
case 'fullwithauthor':
|
||||||
|
case 'encyclopedia':
|
||||||
|
this.showAuthor = true;
|
||||||
|
this.showDate = true;
|
||||||
|
break;
|
||||||
|
case 'fullwithoutauthor':
|
||||||
|
this.showAuthor = false;
|
||||||
|
this.showDate = true;
|
||||||
|
break;
|
||||||
|
default: // Default, and faq, simple, entrylist, continuous.
|
||||||
|
this.showAuthor = false;
|
||||||
|
this.showDate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.glossary;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function called when rating is updated online.
|
* Function called when rating is updated online.
|
||||||
*/
|
*/
|
||||||
ratingUpdated(): void {
|
ratingUpdated(): void {
|
||||||
AddonModGlossary.invalidateEntry(this.entryId);
|
if (!this.onlineEntry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddonModGlossary.invalidateEntry(this.onlineEntry.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -186,13 +326,14 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Helper to manage swiping within a collection of glossary entries.
|
* Helper to manage swiping within a collection of glossary entries.
|
||||||
*/
|
*/
|
||||||
class AddonModGlossaryEntryEntriesSwipeManager extends AddonModGlossaryEntriesSwipeManager {
|
class AddonModGlossaryEntryEntriesSwipeManager
|
||||||
|
extends CoreSwipeNavigationItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null {
|
||||||
return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${route.params.entryId}`;
|
return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${route.params.entrySlug}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fi
|
||||||
import { CoreFile } from '@services/file';
|
import { CoreFile } from '@services/file';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { AddonModGlossaryOffline } from './glossary-offline';
|
import { AddonModGlossaryOffline } from './glossary-offline';
|
||||||
import { AddonModGlossaryNewEntry, AddonModGlossaryNewEntryWithFiles } from './glossary';
|
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
import { CoreFileEntry } from '@services/file-helper';
|
import { CoreFileEntry } from '@services/file-helper';
|
||||||
|
|
||||||
|
@ -58,31 +57,6 @@ export class AddonModGlossaryHelperProvider {
|
||||||
return CoreFileUploader.getStoredFiles(folderPath);
|
return CoreFileUploader.getStoredFiles(folderPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the data of an entry has changed.
|
|
||||||
*
|
|
||||||
* @param entry Current data.
|
|
||||||
* @param files Files attached.
|
|
||||||
* @param original Original content.
|
|
||||||
* @returns True if data has changed, false otherwise.
|
|
||||||
*/
|
|
||||||
hasEntryDataChanged(
|
|
||||||
entry: AddonModGlossaryNewEntry,
|
|
||||||
files: CoreFileEntry[],
|
|
||||||
original?: AddonModGlossaryNewEntryWithFiles,
|
|
||||||
): boolean {
|
|
||||||
if (!original || original.concept === undefined) {
|
|
||||||
// There is no original data.
|
|
||||||
return !!(entry.definition || entry.concept || files.length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (original.definition != entry.definition || original.concept != entry.concept) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CoreFileUploader.areFileListDifferent(files, original.files);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of files (either online files or local files), store the local files in a local folder
|
* Given a list of files (either online files or local files), store the local files in a local folder
|
||||||
* to be submitted later.
|
* to be submitted later.
|
||||||
|
|
|
@ -17,11 +17,11 @@ import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/service
|
||||||
import { CoreFile } from '@services/file';
|
import { CoreFile } from '@services/file';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CorePath } from '@singletons/path';
|
import { CorePath } from '@singletons/path';
|
||||||
import { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary';
|
import { AddonModGlossaryOfflineEntryDBRecord, OFFLINE_ENTRIES_TABLE_NAME } from './database/glossary';
|
||||||
import { AddonModGlossaryDiscardedEntry, AddonModGlossaryEntryOption } from './glossary';
|
import { AddonModGlossaryEntryOption, GLOSSARY_ENTRY_ADDED, GLOSSARY_ENTRY_DELETED, GLOSSARY_ENTRY_UPDATED } from './glossary';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle offline glossary.
|
* Service to handle offline glossary.
|
||||||
|
@ -30,33 +30,33 @@ import { AddonModGlossaryDiscardedEntry, AddonModGlossaryEntryOption } from './g
|
||||||
export class AddonModGlossaryOfflineProvider {
|
export class AddonModGlossaryOfflineProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a new entry.
|
* Delete an offline entry.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary ID.
|
* @param glossaryId Glossary ID.
|
||||||
* @param concept Glossary entry concept.
|
* @param timecreated The time the entry was created.
|
||||||
* @param timeCreated The time the entry was created.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved if deleted, rejected if failure.
|
* @returns Promise resolved if deleted, rejected if failure.
|
||||||
*/
|
*/
|
||||||
async deleteNewEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<void> {
|
async deleteOfflineEntry(glossaryId: number, timecreated: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
|
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
|
||||||
glossaryid: glossaryId,
|
glossaryid: glossaryId,
|
||||||
concept: concept,
|
timecreated: timecreated,
|
||||||
timecreated: timeCreated,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await site.getDb().deleteRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions);
|
await site.getDb().deleteRecords(OFFLINE_ENTRIES_TABLE_NAME, conditions);
|
||||||
|
|
||||||
|
CoreEvents.trigger(GLOSSARY_ENTRY_DELETED, { glossaryId, timecreated });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the stored new entries from all the glossaries.
|
* Get all the stored offline entries from all the glossaries.
|
||||||
*
|
*
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved with entries.
|
* @returns Promise resolved with entries.
|
||||||
*/
|
*/
|
||||||
async getAllNewEntries(siteId?: string): Promise<AddonModGlossaryOfflineEntry[]> {
|
async getAllOfflineEntries(siteId?: string): Promise<AddonModGlossaryOfflineEntry[]> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
const records = await site.getDb().getRecords<AddonModGlossaryOfflineEntryDBRecord>(OFFLINE_ENTRIES_TABLE_NAME);
|
const records = await site.getDb().getRecords<AddonModGlossaryOfflineEntryDBRecord>(OFFLINE_ENTRIES_TABLE_NAME);
|
||||||
|
@ -65,17 +65,15 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a stored new entry.
|
* Get a stored offline entry.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary ID.
|
* @param glossaryId Glossary ID.
|
||||||
* @param concept Glossary entry concept.
|
|
||||||
* @param timeCreated The time the entry was created.
|
* @param timeCreated The time the entry was created.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved with entry.
|
* @returns Promise resolved with entry.
|
||||||
*/
|
*/
|
||||||
async getNewEntry(
|
async getOfflineEntry(
|
||||||
glossaryId: number,
|
glossaryId: number,
|
||||||
concept: string,
|
|
||||||
timeCreated: number,
|
timeCreated: number,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<AddonModGlossaryOfflineEntry> {
|
): Promise<AddonModGlossaryOfflineEntry> {
|
||||||
|
@ -83,7 +81,6 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
|
|
||||||
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
|
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
|
||||||
glossaryid: glossaryId,
|
glossaryid: glossaryId,
|
||||||
concept: concept,
|
|
||||||
timecreated: timeCreated,
|
timecreated: timeCreated,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -100,7 +97,7 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
* @param userId User the entries belong to. If not defined, current user in site.
|
* @param userId User the entries belong to. If not defined, current user in site.
|
||||||
* @returns Promise resolved with entries.
|
* @returns Promise resolved with entries.
|
||||||
*/
|
*/
|
||||||
async getGlossaryNewEntries(glossaryId: number, siteId?: string, userId?: number): Promise<AddonModGlossaryOfflineEntry[]> {
|
async getGlossaryOfflineEntries(glossaryId: number, siteId?: string, userId?: number): Promise<AddonModGlossaryOfflineEntry[]> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
|
const conditions: Partial<AddonModGlossaryOfflineEntryDBRecord> = {
|
||||||
|
@ -143,7 +140,7 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's only one entry, check that is not the one we are editing.
|
// If there's only one entry, check that is not the one we are editing.
|
||||||
return CoreUtils.promiseFails(this.getNewEntry(glossaryId, concept, timeCreated, siteId));
|
return entries[0].timecreated !== timeCreated;
|
||||||
} catch {
|
} catch {
|
||||||
// No offline data found, return false.
|
// No offline data found, return false.
|
||||||
return false;
|
return false;
|
||||||
|
@ -151,31 +148,29 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a new entry to be sent later.
|
* Save an offline entry to be sent later.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary ID.
|
* @param glossaryId Glossary ID.
|
||||||
* @param concept Glossary entry concept.
|
* @param concept Glossary entry concept.
|
||||||
* @param definition Glossary entry concept definition.
|
* @param definition Glossary entry concept definition.
|
||||||
* @param courseId Course ID of the glossary.
|
* @param courseId Course ID of the glossary.
|
||||||
|
* @param timecreated The time the entry was created. If not defined, current time.
|
||||||
* @param options Options for the entry.
|
* @param options Options for the entry.
|
||||||
* @param attachments Result of CoreFileUploaderProvider#storeFilesToUpload for attachments.
|
* @param attachments Result of CoreFileUploaderProvider#storeFilesToUpload for attachments.
|
||||||
* @param timeCreated The time the entry was created. If not defined, current time.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @param userId User the entry belong to. If not defined, current user in site.
|
* @param userId User the entry belong to. If not defined, current user in site.
|
||||||
* @param discardEntry The entry provided will be discarded if found.
|
|
||||||
* @returns Promise resolved if stored, rejected if failure.
|
* @returns Promise resolved if stored, rejected if failure.
|
||||||
*/
|
*/
|
||||||
async addNewEntry(
|
async addOfflineEntry(
|
||||||
glossaryId: number,
|
glossaryId: number,
|
||||||
concept: string,
|
concept: string,
|
||||||
definition: string,
|
definition: string,
|
||||||
courseId: number,
|
courseId: number,
|
||||||
|
timecreated: number,
|
||||||
options?: Record<string, AddonModGlossaryEntryOption>,
|
options?: Record<string, AddonModGlossaryEntryOption>,
|
||||||
attachments?: CoreFileUploaderStoreFilesResult,
|
attachments?: CoreFileUploaderStoreFilesResult,
|
||||||
timeCreated?: number,
|
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
discardEntry?: AddonModGlossaryDiscardedEntry,
|
|
||||||
): Promise<false> {
|
): Promise<false> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
@ -188,19 +183,52 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
options: JSON.stringify(options || {}),
|
options: JSON.stringify(options || {}),
|
||||||
attachments: JSON.stringify(attachments),
|
attachments: JSON.stringify(attachments),
|
||||||
userid: userId || site.getUserId(),
|
userid: userId || site.getUserId(),
|
||||||
timecreated: timeCreated || Date.now(),
|
timecreated,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If editing an offline entry, delete previous first.
|
|
||||||
if (discardEntry) {
|
|
||||||
await this.deleteNewEntry(glossaryId, discardEntry.concept, discardEntry.timecreated, site.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry);
|
await site.getDb().insertRecord(OFFLINE_ENTRIES_TABLE_NAME, entry);
|
||||||
|
|
||||||
|
CoreEvents.trigger(GLOSSARY_ENTRY_ADDED, { glossaryId, timecreated }, siteId);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an offline entry to be sent later.
|
||||||
|
*
|
||||||
|
* @param originalEntry Original entry data.
|
||||||
|
* @param concept Glossary entry concept.
|
||||||
|
* @param definition Glossary entry concept definition.
|
||||||
|
* @param options Options for the entry.
|
||||||
|
* @param attachments Result of CoreFileUploaderProvider#storeFilesToUpload for attachments.
|
||||||
|
*/
|
||||||
|
async updateOfflineEntry(
|
||||||
|
originalEntry: Pick< AddonModGlossaryOfflineEntryDBRecord, 'glossaryid'|'courseid'|'concept'|'timecreated'>,
|
||||||
|
concept: string,
|
||||||
|
definition: string,
|
||||||
|
options?: Record<string, AddonModGlossaryEntryOption>,
|
||||||
|
attachments?: CoreFileUploaderStoreFilesResult,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite();
|
||||||
|
const entry: Omit<AddonModGlossaryOfflineEntryDBRecord, 'courseid'|'glossaryid'|'userid'|'timecreated'> = {
|
||||||
|
concept: concept,
|
||||||
|
definition: definition,
|
||||||
|
definitionformat: 'html',
|
||||||
|
options: JSON.stringify(options || {}),
|
||||||
|
attachments: JSON.stringify(attachments),
|
||||||
|
};
|
||||||
|
|
||||||
|
await site.getDb().updateRecords(OFFLINE_ENTRIES_TABLE_NAME, entry, {
|
||||||
|
...originalEntry,
|
||||||
|
userid: site.getUserId(),
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.trigger(GLOSSARY_ENTRY_UPDATED, {
|
||||||
|
glossaryId: originalEntry.glossaryid,
|
||||||
|
timecreated: originalEntry.timecreated,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path to the folder where to store files for offline attachments in a glossary.
|
* Get the path to the folder where to store files for offline attachments in a glossary.
|
||||||
*
|
*
|
||||||
|
@ -218,7 +246,7 @@ export class AddonModGlossaryOfflineProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path to the folder where to store files for a new offline entry.
|
* Get the path to the folder where to store files for an offline entry.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary ID.
|
* @param glossaryId Glossary ID.
|
||||||
* @param concept The name of the entry.
|
* @param concept The name of the entry.
|
||||||
|
|
|
@ -31,14 +31,14 @@ import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from './glossar
|
||||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
|
||||||
import { CoreFileEntry } from '@services/file-helper';
|
import { CoreFileEntry } from '@services/file-helper';
|
||||||
|
|
||||||
|
export const GLOSSARY_AUTO_SYNCED = 'addon_mod_glossary_auto_synced';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to sync glossaries.
|
* Service to sync glossaries.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModGlossarySyncResult> {
|
export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModGlossarySyncResult> {
|
||||||
|
|
||||||
static readonly AUTO_SYNCED = 'addon_mod_glossary_autom_synced';
|
|
||||||
|
|
||||||
protected componentTranslatableString = 'glossary';
|
protected componentTranslatableString = 'glossary';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -50,10 +50,9 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
|
||||||
*
|
*
|
||||||
* @param siteId Site ID to sync. If not defined, sync all sites.
|
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||||
* @param force Wether to force sync not depending on last execution.
|
* @param force Wether to force sync not depending on last execution.
|
||||||
* @returns Promise resolved if sync is successful, rejected if sync fails.
|
|
||||||
*/
|
*/
|
||||||
syncAllGlossaries(siteId?: string, force?: boolean): Promise<void> {
|
async syncAllGlossaries(siteId?: string, force?: boolean): Promise<void> {
|
||||||
return this.syncOnSites('all glossaries', (siteId) => this.syncAllGlossariesFunc(!!force, siteId), siteId);
|
await this.syncOnSites('all glossaries', (siteId) => this.syncAllGlossariesFunc(!!force, siteId), siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,7 +60,6 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
|
||||||
*
|
*
|
||||||
* @param force Wether to force sync not depending on last execution.
|
* @param force Wether to force sync not depending on last execution.
|
||||||
* @param siteId Site ID to sync.
|
* @param siteId Site ID to sync.
|
||||||
* @returns Promise resolved if sync is successful, rejected if sync fails.
|
|
||||||
*/
|
*/
|
||||||
protected async syncAllGlossariesFunc(force: boolean, siteId: string): Promise<void> {
|
protected async syncAllGlossariesFunc(force: boolean, siteId: string): Promise<void> {
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
@ -73,14 +71,13 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync entried of all glossaries on a site.
|
* Sync entries of all glossaries on a site.
|
||||||
*
|
*
|
||||||
* @param force Wether to force sync not depending on last execution.
|
* @param force Wether to force sync not depending on last execution.
|
||||||
* @param siteId Site ID to sync.
|
* @param siteId Site ID to sync.
|
||||||
* @returns Promise resolved if sync is successful, rejected if sync fails.
|
|
||||||
*/
|
*/
|
||||||
protected async syncAllGlossariesEntries(force: boolean, siteId: string): Promise<void> {
|
protected async syncAllGlossariesEntries(force: boolean, siteId: string): Promise<void> {
|
||||||
const entries = await AddonModGlossaryOffline.getAllNewEntries(siteId);
|
const entries = await AddonModGlossaryOffline.getAllOfflineEntries(siteId);
|
||||||
|
|
||||||
// Do not sync same glossary twice.
|
// Do not sync same glossary twice.
|
||||||
const treated: Record<number, boolean> = {};
|
const treated: Record<number, boolean> = {};
|
||||||
|
@ -98,7 +95,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
|
||||||
|
|
||||||
if (result?.updated) {
|
if (result?.updated) {
|
||||||
// Sync successful, send event.
|
// Sync successful, send event.
|
||||||
CoreEvents.trigger(AddonModGlossarySyncProvider.AUTO_SYNCED, {
|
CoreEvents.trigger(GLOSSARY_AUTO_SYNCED, {
|
||||||
glossaryId: entry.glossaryid,
|
glossaryId: entry.glossaryid,
|
||||||
userId: entry.userid,
|
userId: entry.userid,
|
||||||
warnings: result.warnings,
|
warnings: result.warnings,
|
||||||
|
@ -180,7 +177,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
|
||||||
|
|
||||||
// Get offline responses to be sent.
|
// Get offline responses to be sent.
|
||||||
const entries = await CoreUtils.ignoreErrors(
|
const entries = await CoreUtils.ignoreErrors(
|
||||||
AddonModGlossaryOffline.getGlossaryNewEntries(glossaryId, siteId, userId),
|
AddonModGlossaryOffline.getGlossaryOfflineEntries(glossaryId, siteId, userId),
|
||||||
<AddonModGlossaryOfflineEntry[]> [],
|
<AddonModGlossaryOfflineEntry[]> [],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -285,11 +282,10 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
|
||||||
* @param concept Glossary entry concept.
|
* @param concept Glossary entry concept.
|
||||||
* @param timeCreated Time to allow duplicated entries.
|
* @param timeCreated Time to allow duplicated entries.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved when deleted.
|
|
||||||
*/
|
*/
|
||||||
protected async deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<void> {
|
protected async deleteAddEntry(glossaryId: number, concept: string, timeCreated: number, siteId?: string): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
AddonModGlossaryOffline.deleteNewEntry(glossaryId, concept, timeCreated, siteId),
|
AddonModGlossaryOffline.deleteOfflineEntry(glossaryId, timeCreated, siteId),
|
||||||
AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId),
|
AddonModGlossaryHelper.deleteStoredFiles(glossaryId, concept, timeCreated, siteId),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -341,15 +337,28 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv
|
||||||
|
|
||||||
export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider);
|
export const AddonModGlossarySync = makeSingleton(AddonModGlossarySyncProvider);
|
||||||
|
|
||||||
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Augment CoreEventsData interface with events specific to this service.
|
||||||
|
*
|
||||||
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||||
|
*/
|
||||||
|
export interface CoreEventsData {
|
||||||
|
[GLOSSARY_AUTO_SYNCED]: AddonModGlossaryAutoSyncedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data returned by a glossary sync.
|
* Data returned by a glossary sync.
|
||||||
*/
|
*/
|
||||||
export type AddonModGlossarySyncResult = CoreSyncResult;
|
export type AddonModGlossarySyncResult = CoreSyncResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data passed to AUTO_SYNCED event.
|
* Data passed to GLOSSARY_AUTO_SYNCED event.
|
||||||
*/
|
*/
|
||||||
export type AddonModGlossaryAutoSyncData = {
|
export type AddonModGlossaryAutoSyncedData = {
|
||||||
glossaryId: number;
|
glossaryId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
|
|
|
@ -25,12 +25,13 @@ import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary';
|
import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary';
|
||||||
import { AddonModGlossaryOffline } from './glossary-offline';
|
import { AddonModGlossaryOffline } from './glossary-offline';
|
||||||
import { AddonModGlossaryAutoSyncData, AddonModGlossarySyncProvider } from './glossary-sync';
|
|
||||||
import { CoreFileEntry } from '@services/file-helper';
|
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmaModGlossary:';
|
export const GLOSSARY_ENTRY_ADDED = 'addon_mod_glossary_entry_added';
|
||||||
|
export const GLOSSARY_ENTRY_UPDATED = 'addon_mod_glossary_entry_updated';
|
||||||
|
export const GLOSSARY_ENTRY_DELETED = 'addon_mod_glossary_entry_deleted';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that provides some features for glossaries.
|
* Service that provides some features for glossaries.
|
||||||
|
@ -41,10 +42,9 @@ export class AddonModGlossaryProvider {
|
||||||
static readonly COMPONENT = 'mmaModGlossary';
|
static readonly COMPONENT = 'mmaModGlossary';
|
||||||
static readonly LIMIT_ENTRIES = 25;
|
static readonly LIMIT_ENTRIES = 25;
|
||||||
static readonly LIMIT_CATEGORIES = 10;
|
static readonly LIMIT_CATEGORIES = 10;
|
||||||
static readonly SHOW_ALL_CATEGORIES = 0;
|
|
||||||
static readonly SHOW_NOT_CATEGORISED = -1;
|
|
||||||
|
|
||||||
static readonly ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry';
|
private static readonly SHOW_ALL_CATEGORIES = 0;
|
||||||
|
private static readonly ROOT_CACHE_KEY = 'mmaModGlossary:';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the course glossary cache key.
|
* Get the course glossary cache key.
|
||||||
|
@ -53,7 +53,7 @@ export class AddonModGlossaryProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getCourseGlossariesCacheKey(courseId: number): string {
|
protected getCourseGlossariesCacheKey(courseId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'courseGlossaries:' + courseId;
|
return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}courseGlossaries:${courseId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,7 +90,6 @@ export class AddonModGlossaryProvider {
|
||||||
*
|
*
|
||||||
* @param courseId Course Id.
|
* @param courseId Course Id.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
async invalidateCourseGlossaries(courseId: number, siteId?: string): Promise<void> {
|
async invalidateCourseGlossaries(courseId: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
@ -104,44 +103,35 @@ export class AddonModGlossaryProvider {
|
||||||
* Get the entries by author cache key.
|
* Get the entries by author cache key.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL.
|
|
||||||
* @param field Search and order using: FIRSTNAME or LASTNAME
|
|
||||||
* @param sort The direction of the order: ASC or DESC
|
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getEntriesByAuthorCacheKey(glossaryId: number, letter: string, field: string, sort: string): string {
|
protected getEntriesByAuthorCacheKey(glossaryId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'entriesByAuthor:' + glossaryId + ':' + letter + ':' + field + ':' + sort;
|
return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByAuthor:${glossaryId}:ALL:LASTNAME:ASC`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get entries by author.
|
* Get entries by author.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL.
|
|
||||||
* @param field Search and order using: FIRSTNAME or LASTNAME
|
|
||||||
* @param sort The direction of the order: ASC or DESC
|
|
||||||
* @param options Other options.
|
* @param options Other options.
|
||||||
* @returns Resolved with the entries.
|
* @returns Resolved with the entries.
|
||||||
*/
|
*/
|
||||||
async getEntriesByAuthor(
|
async getEntriesByAuthor(
|
||||||
glossaryId: number,
|
glossaryId: number,
|
||||||
letter: string,
|
|
||||||
field: string,
|
|
||||||
sort: string,
|
|
||||||
options: AddonModGlossaryGetEntriesOptions = {},
|
options: AddonModGlossaryGetEntriesOptions = {},
|
||||||
): Promise<AddonModGlossaryGetEntriesWSResponse> {
|
): Promise<AddonModGlossaryGetEntriesWSResponse> {
|
||||||
const site = await CoreSites.getSite(options.siteId);
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
const params: AddonModGlossaryGetEntriesByAuthorWSParams = {
|
const params: AddonModGlossaryGetEntriesByAuthorWSParams = {
|
||||||
id: glossaryId,
|
id: glossaryId,
|
||||||
letter: letter,
|
letter: 'ALL',
|
||||||
field: field,
|
field: 'LASTNAME',
|
||||||
sort: sort,
|
sort: 'ASC',
|
||||||
from: options.from || 0,
|
from: options.from || 0,
|
||||||
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
|
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort),
|
cacheKey: this.getEntriesByAuthorCacheKey(glossaryId),
|
||||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
component: AddonModGlossaryProvider.COMPONENT,
|
component: AddonModGlossaryProvider.COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
|
@ -155,22 +145,12 @@ export class AddonModGlossaryProvider {
|
||||||
* Invalidate cache of entries by author.
|
* Invalidate cache of entries by author.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param letter First letter of firstname or lastname, or either keywords: ALL or SPECIAL.
|
|
||||||
* @param field Search and order using: FIRSTNAME or LASTNAME
|
|
||||||
* @param sort The direction of the order: ASC or DESC
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
async invalidateEntriesByAuthor(
|
async invalidateEntriesByAuthor(glossaryId: number, siteId?: string): Promise<void> {
|
||||||
glossaryId: number,
|
|
||||||
letter: string,
|
|
||||||
field: string,
|
|
||||||
sort: string,
|
|
||||||
siteId?: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
const key = this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort);
|
const key = this.getEntriesByAuthorCacheKey(glossaryId);
|
||||||
|
|
||||||
await site.invalidateWsCacheForKey(key);
|
await site.invalidateWsCacheForKey(key);
|
||||||
}
|
}
|
||||||
|
@ -179,26 +159,23 @@ export class AddonModGlossaryProvider {
|
||||||
* Get entries by category.
|
* Get entries by category.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or
|
|
||||||
* constant SHOW_NOT_CATEGORISED for uncategorised entries.
|
|
||||||
* @param options Other options.
|
* @param options Other options.
|
||||||
* @returns Resolved with the entries.
|
* @returns Resolved with the entries.
|
||||||
*/
|
*/
|
||||||
async getEntriesByCategory(
|
async getEntriesByCategory(
|
||||||
glossaryId: number,
|
glossaryId: number,
|
||||||
categoryId: number,
|
|
||||||
options: AddonModGlossaryGetEntriesOptions = {},
|
options: AddonModGlossaryGetEntriesOptions = {},
|
||||||
): Promise<AddonModGlossaryGetEntriesByCategoryWSResponse> {
|
): Promise<AddonModGlossaryGetEntriesByCategoryWSResponse> {
|
||||||
const site = await CoreSites.getSite(options.siteId);
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
const params: AddonModGlossaryGetEntriesByCategoryWSParams = {
|
const params: AddonModGlossaryGetEntriesByCategoryWSParams = {
|
||||||
id: glossaryId,
|
id: glossaryId,
|
||||||
categoryid: categoryId,
|
categoryid: AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
||||||
from: options.from || 0,
|
from: options.from || 0,
|
||||||
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
|
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId),
|
cacheKey: this.getEntriesByCategoryCacheKey(glossaryId),
|
||||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
component: AddonModGlossaryProvider.COMPONENT,
|
component: AddonModGlossaryProvider.COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
|
@ -212,15 +189,12 @@ export class AddonModGlossaryProvider {
|
||||||
* Invalidate cache of entries by category.
|
* Invalidate cache of entries by category.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or
|
|
||||||
* constant SHOW_NOT_CATEGORISED for uncategorised entries.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
async invalidateEntriesByCategory(glossaryId: number, categoryId: number, siteId?: string): Promise<void> {
|
async invalidateEntriesByCategory(glossaryId: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
const key = this.getEntriesByCategoryCacheKey(glossaryId, categoryId);
|
const key = this.getEntriesByCategoryCacheKey(glossaryId);
|
||||||
|
|
||||||
await site.invalidateWsCacheForKey(key);
|
await site.invalidateWsCacheForKey(key);
|
||||||
}
|
}
|
||||||
|
@ -229,12 +203,12 @@ export class AddonModGlossaryProvider {
|
||||||
* Get the entries by category cache key.
|
* Get the entries by category cache key.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or
|
|
||||||
* constant SHOW_NOT_CATEGORISED for uncategorised entries.
|
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
getEntriesByCategoryCacheKey(glossaryId: number, categoryId: number): string {
|
getEntriesByCategoryCacheKey(glossaryId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'entriesByCategory:' + glossaryId + ':' + categoryId;
|
const prefix = `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByCategory`;
|
||||||
|
|
||||||
|
return `${prefix}:${glossaryId}:${AddonModGlossaryProvider.SHOW_ALL_CATEGORIES}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -242,11 +216,10 @@ export class AddonModGlossaryProvider {
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param order The way to order the records.
|
* @param order The way to order the records.
|
||||||
* @param sort The direction of the order.
|
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
getEntriesByDateCacheKey(glossaryId: number, order: string, sort: string): string {
|
getEntriesByDateCacheKey(glossaryId: number, order: string): string {
|
||||||
return ROOT_CACHE_KEY + 'entriesByDate:' + glossaryId + ':' + order + ':' + sort;
|
return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByDate:${glossaryId}:${order}:DESC`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -254,14 +227,12 @@ export class AddonModGlossaryProvider {
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param order The way to order the records.
|
* @param order The way to order the records.
|
||||||
* @param sort The direction of the order.
|
|
||||||
* @param options Other options.
|
* @param options Other options.
|
||||||
* @returns Resolved with the entries.
|
* @returns Resolved with the entries.
|
||||||
*/
|
*/
|
||||||
async getEntriesByDate(
|
async getEntriesByDate(
|
||||||
glossaryId: number,
|
glossaryId: number,
|
||||||
order: string,
|
order: string,
|
||||||
sort: string,
|
|
||||||
options: AddonModGlossaryGetEntriesOptions = {},
|
options: AddonModGlossaryGetEntriesOptions = {},
|
||||||
): Promise<AddonModGlossaryGetEntriesWSResponse> {
|
): Promise<AddonModGlossaryGetEntriesWSResponse> {
|
||||||
const site = await CoreSites.getSite(options.siteId);
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
@ -269,12 +240,12 @@ export class AddonModGlossaryProvider {
|
||||||
const params: AddonModGlossaryGetEntriesByDateWSParams = {
|
const params: AddonModGlossaryGetEntriesByDateWSParams = {
|
||||||
id: glossaryId,
|
id: glossaryId,
|
||||||
order: order,
|
order: order,
|
||||||
sort: sort,
|
sort: 'DESC',
|
||||||
from: options.from || 0,
|
from: options.from || 0,
|
||||||
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
|
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort),
|
cacheKey: this.getEntriesByDateCacheKey(glossaryId, order),
|
||||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
component: AddonModGlossaryProvider.COMPONENT,
|
component: AddonModGlossaryProvider.COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
|
@ -289,14 +260,12 @@ export class AddonModGlossaryProvider {
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param order The way to order the records.
|
* @param order The way to order the records.
|
||||||
* @param sort The direction of the order.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
async invalidateEntriesByDate(glossaryId: number, order: string, sort: string, siteId?: string): Promise<void> {
|
async invalidateEntriesByDate(glossaryId: number, order: string, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
const key = this.getEntriesByDateCacheKey(glossaryId, order, sort);
|
const key = this.getEntriesByDateCacheKey(glossaryId, order);
|
||||||
|
|
||||||
await site.invalidateWsCacheForKey(key);
|
await site.invalidateWsCacheForKey(key);
|
||||||
}
|
}
|
||||||
|
@ -305,24 +274,21 @@ export class AddonModGlossaryProvider {
|
||||||
* Get the entries by letter cache key.
|
* Get the entries by letter cache key.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param letter A letter, or a special keyword.
|
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getEntriesByLetterCacheKey(glossaryId: number, letter: string): string {
|
protected getEntriesByLetterCacheKey(glossaryId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'entriesByLetter:' + glossaryId + ':' + letter;
|
return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesByLetter:${glossaryId}:ALL`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get entries by letter.
|
* Get entries by letter.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param letter A letter, or a special keyword.
|
|
||||||
* @param options Other options.
|
* @param options Other options.
|
||||||
* @returns Resolved with the entries.
|
* @returns Resolved with the entries.
|
||||||
*/
|
*/
|
||||||
async getEntriesByLetter(
|
async getEntriesByLetter(
|
||||||
glossaryId: number,
|
glossaryId: number,
|
||||||
letter: string,
|
|
||||||
options: AddonModGlossaryGetEntriesOptions = {},
|
options: AddonModGlossaryGetEntriesOptions = {},
|
||||||
): Promise<AddonModGlossaryGetEntriesWSResponse> {
|
): Promise<AddonModGlossaryGetEntriesWSResponse> {
|
||||||
options.from = options.from || 0;
|
options.from = options.from || 0;
|
||||||
|
@ -332,12 +298,12 @@ export class AddonModGlossaryProvider {
|
||||||
|
|
||||||
const params: AddonModGlossaryGetEntriesByLetterWSParams = {
|
const params: AddonModGlossaryGetEntriesByLetterWSParams = {
|
||||||
id: glossaryId,
|
id: glossaryId,
|
||||||
letter: letter,
|
letter: 'ALL',
|
||||||
from: options.from,
|
from: options.from,
|
||||||
limit: options.limit,
|
limit: options.limit,
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter),
|
cacheKey: this.getEntriesByLetterCacheKey(glossaryId),
|
||||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
component: AddonModGlossaryProvider.COMPONENT,
|
component: AddonModGlossaryProvider.COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
|
@ -362,16 +328,14 @@ export class AddonModGlossaryProvider {
|
||||||
* Invalidate cache of entries by letter.
|
* Invalidate cache of entries by letter.
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param letter A letter, or a special keyword.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
async invalidateEntriesByLetter(glossaryId: number, letter: string, siteId?: string): Promise<void> {
|
async invalidateEntriesByLetter(glossaryId: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
const key = this.getEntriesByLetterCacheKey(glossaryId, letter);
|
const key = this.getEntriesByLetterCacheKey(glossaryId);
|
||||||
|
|
||||||
return site.invalidateWsCacheForKey(key);
|
await site.invalidateWsCacheForKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -380,18 +344,10 @@ export class AddonModGlossaryProvider {
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param query The search query.
|
* @param query The search query.
|
||||||
* @param fullSearch Whether or not full search is required.
|
* @param fullSearch Whether or not full search is required.
|
||||||
* @param order The way to order the results.
|
|
||||||
* @param sort The direction of the order.
|
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getEntriesBySearchCacheKey(
|
protected getEntriesBySearchCacheKey(glossaryId: number, query: string, fullSearch: boolean): string {
|
||||||
glossaryId: number,
|
return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}entriesBySearch:${glossaryId}:${fullSearch}:CONCEPT:ASC:${query}`;
|
||||||
query: string,
|
|
||||||
fullSearch: boolean,
|
|
||||||
order: string,
|
|
||||||
sort: string,
|
|
||||||
): string {
|
|
||||||
return ROOT_CACHE_KEY + 'entriesBySearch:' + glossaryId + ':' + fullSearch + ':' + order + ':' + sort + ':' + query;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -400,8 +356,6 @@ export class AddonModGlossaryProvider {
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param query The search query.
|
* @param query The search query.
|
||||||
* @param fullSearch Whether or not full search is required.
|
* @param fullSearch Whether or not full search is required.
|
||||||
* @param order The way to order the results.
|
|
||||||
* @param sort The direction of the order.
|
|
||||||
* @param options Get entries options.
|
* @param options Get entries options.
|
||||||
* @returns Resolved with the entries.
|
* @returns Resolved with the entries.
|
||||||
*/
|
*/
|
||||||
|
@ -409,8 +363,6 @@ export class AddonModGlossaryProvider {
|
||||||
glossaryId: number,
|
glossaryId: number,
|
||||||
query: string,
|
query: string,
|
||||||
fullSearch: boolean,
|
fullSearch: boolean,
|
||||||
order: string,
|
|
||||||
sort: string,
|
|
||||||
options: AddonModGlossaryGetEntriesOptions = {},
|
options: AddonModGlossaryGetEntriesOptions = {},
|
||||||
): Promise<AddonModGlossaryGetEntriesWSResponse> {
|
): Promise<AddonModGlossaryGetEntriesWSResponse> {
|
||||||
const site = await CoreSites.getSite(options.siteId);
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
@ -419,13 +371,13 @@ export class AddonModGlossaryProvider {
|
||||||
id: glossaryId,
|
id: glossaryId,
|
||||||
query: query,
|
query: query,
|
||||||
fullsearch: fullSearch,
|
fullsearch: fullSearch,
|
||||||
order: order,
|
order: 'CONCEPT',
|
||||||
sort: sort,
|
sort: 'ASC',
|
||||||
from: options.from || 0,
|
from: options.from || 0,
|
||||||
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
|
limit: options.limit || AddonModGlossaryProvider.LIMIT_ENTRIES,
|
||||||
};
|
};
|
||||||
const preSets: CoreSiteWSPreSets = {
|
const preSets: CoreSiteWSPreSets = {
|
||||||
cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort),
|
cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch),
|
||||||
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
updateFrequency: CoreSite.FREQUENCY_SOMETIMES,
|
||||||
component: AddonModGlossaryProvider.COMPONENT,
|
component: AddonModGlossaryProvider.COMPONENT,
|
||||||
componentId: options.cmId,
|
componentId: options.cmId,
|
||||||
|
@ -441,22 +393,17 @@ export class AddonModGlossaryProvider {
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param query The search query.
|
* @param query The search query.
|
||||||
* @param fullSearch Whether or not full search is required.
|
* @param fullSearch Whether or not full search is required.
|
||||||
* @param order The way to order the results.
|
|
||||||
* @param sort The direction of the order.
|
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
async invalidateEntriesBySearch(
|
async invalidateEntriesBySearch(
|
||||||
glossaryId: number,
|
glossaryId: number,
|
||||||
query: string,
|
query: string,
|
||||||
fullSearch: boolean,
|
fullSearch: boolean,
|
||||||
order: string,
|
|
||||||
sort: string,
|
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
const key = this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort);
|
const key = this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch);
|
||||||
|
|
||||||
await site.invalidateWsCacheForKey(key);
|
await site.invalidateWsCacheForKey(key);
|
||||||
}
|
}
|
||||||
|
@ -468,7 +415,7 @@ export class AddonModGlossaryProvider {
|
||||||
* @returns The cache key.
|
* @returns The cache key.
|
||||||
*/
|
*/
|
||||||
protected getCategoriesCacheKey(glossaryId: number): string {
|
protected getCategoriesCacheKey(glossaryId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'categories:' + glossaryId;
|
return AddonModGlossaryProvider.ROOT_CACHE_KEY + 'categories:' + glossaryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -533,7 +480,6 @@ export class AddonModGlossaryProvider {
|
||||||
*
|
*
|
||||||
* @param glossaryId Glossary Id.
|
* @param glossaryId Glossary Id.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved when categories data has been invalidated,
|
|
||||||
*/
|
*/
|
||||||
async invalidateCategories(glossaryId: number, siteId?: string): Promise<void> {
|
async invalidateCategories(glossaryId: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
@ -548,7 +494,7 @@ export class AddonModGlossaryProvider {
|
||||||
* @returns Cache key.
|
* @returns Cache key.
|
||||||
*/
|
*/
|
||||||
protected getEntryCacheKey(entryId: number): string {
|
protected getEntryCacheKey(entryId: number): string {
|
||||||
return ROOT_CACHE_KEY + 'getEntry:' + entryId;
|
return `${AddonModGlossaryProvider.ROOT_CACHE_KEY}getEntry:${entryId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -637,7 +583,7 @@ export class AddonModGlossaryProvider {
|
||||||
options: CoreCourseCommonModWSOptions = {},
|
options: CoreCourseCommonModWSOptions = {},
|
||||||
): Promise<AddonModGlossaryGetEntryByIdResponse> {
|
): Promise<AddonModGlossaryGetEntryByIdResponse> {
|
||||||
// Get the entries from this "page" and check if the entry we're looking for is in it.
|
// Get the entries from this "page" and check if the entry we're looking for is in it.
|
||||||
const result = await this.getEntriesByLetter(glossaryId, 'ALL', {
|
const result = await this.getEntriesByLetter(glossaryId, {
|
||||||
from: from,
|
from: from,
|
||||||
readingStrategy: CoreSitesReadingStrategy.ONLY_CACHE,
|
readingStrategy: CoreSitesReadingStrategy.ONLY_CACHE,
|
||||||
cmId: options.cmId,
|
cmId: options.cmId,
|
||||||
|
@ -661,6 +607,30 @@ export class AddonModGlossaryProvider {
|
||||||
throw new CoreError('Entry not found.');
|
throw new CoreError('Entry not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the site can delete glossary entries.
|
||||||
|
*
|
||||||
|
* @param siteId Site id.
|
||||||
|
* @returns Whether the site can delete entries.
|
||||||
|
*/
|
||||||
|
async canDeleteEntries(siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return site.wsAvailable('mod_glossary_delete_entry');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the site can update glossary entries.
|
||||||
|
*
|
||||||
|
* @param siteId Site id.
|
||||||
|
* @returns Whether the site can update entries.
|
||||||
|
*/
|
||||||
|
async canUpdateEntries(siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
return site.wsAvailable('mod_glossary_update_entry');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs the whole fetch of the entries using the proper function and arguments.
|
* Performs the whole fetch of the entries using the proper function and arguments.
|
||||||
*
|
*
|
||||||
|
@ -695,7 +665,6 @@ export class AddonModGlossaryProvider {
|
||||||
*
|
*
|
||||||
* @param entryId Entry Id.
|
* @param entryId Entry Id.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
async invalidateEntry(entryId: number, siteId?: string): Promise<void> {
|
async invalidateEntry(entryId: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
@ -708,7 +677,6 @@ export class AddonModGlossaryProvider {
|
||||||
*
|
*
|
||||||
* @param entries Entry objects to invalidate.
|
* @param entries Entry objects to invalidate.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
protected async invalidateEntries(entries: AddonModGlossaryEntry[], siteId?: string): Promise<void> {
|
protected async invalidateEntries(entries: AddonModGlossaryEntry[], siteId?: string): Promise<void> {
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
|
@ -727,7 +695,6 @@ export class AddonModGlossaryProvider {
|
||||||
*
|
*
|
||||||
* @param moduleId The module ID.
|
* @param moduleId The module ID.
|
||||||
* @param courseId Course ID.
|
* @param courseId Course ID.
|
||||||
* @returns Promise resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
||||||
const glossary = await this.getGlossary(courseId, moduleId);
|
const glossary = await this.getGlossary(courseId, moduleId);
|
||||||
|
@ -747,7 +714,6 @@ export class AddonModGlossaryProvider {
|
||||||
* @param glossary The glossary object.
|
* @param glossary The glossary object.
|
||||||
* @param onlyEntriesList If true, entries won't be invalidated.
|
* @param onlyEntriesList If true, entries won't be invalidated.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved when data is invalidated.
|
|
||||||
*/
|
*/
|
||||||
async invalidateGlossaryEntries(glossary: AddonModGlossaryGlossary, onlyEntriesList?: boolean, siteId?: string): Promise<void> {
|
async invalidateGlossaryEntries(glossary: AddonModGlossaryGlossary, onlyEntriesList?: boolean, siteId?: string): Promise<void> {
|
||||||
siteId = siteId || CoreSites.getCurrentSiteId();
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
@ -755,7 +721,7 @@ export class AddonModGlossaryProvider {
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
if (!onlyEntriesList) {
|
if (!onlyEntriesList) {
|
||||||
promises.push(this.fetchAllEntries((options) => this.getEntriesByLetter(glossary.id, 'ALL', options), {
|
promises.push(this.fetchAllEntries((options) => this.getEntriesByLetter(glossary.id, options), {
|
||||||
cmId: glossary.coursemodule,
|
cmId: glossary.coursemodule,
|
||||||
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
|
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
|
||||||
siteId,
|
siteId,
|
||||||
|
@ -765,21 +731,17 @@ export class AddonModGlossaryProvider {
|
||||||
glossary.browsemodes.forEach((mode) => {
|
glossary.browsemodes.forEach((mode) => {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'letter':
|
case 'letter':
|
||||||
promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL', siteId));
|
promises.push(this.invalidateEntriesByLetter(glossary.id, siteId));
|
||||||
break;
|
break;
|
||||||
case 'cat':
|
case 'cat':
|
||||||
promises.push(this.invalidateEntriesByCategory(
|
promises.push(this.invalidateEntriesByCategory(glossary.id, siteId));
|
||||||
glossary.id,
|
|
||||||
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
|
||||||
siteId,
|
|
||||||
));
|
|
||||||
break;
|
break;
|
||||||
case 'date':
|
case 'date':
|
||||||
promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', 'DESC', siteId));
|
promises.push(this.invalidateEntriesByDate(glossary.id, 'CREATION', siteId));
|
||||||
promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', 'DESC', siteId));
|
promises.push(this.invalidateEntriesByDate(glossary.id, 'UPDATE', siteId));
|
||||||
break;
|
break;
|
||||||
case 'author':
|
case 'author':
|
||||||
promises.push(this.invalidateEntriesByAuthor(glossary.id, 'ALL', 'LASTNAME', 'ASC', siteId));
|
promises.push(this.invalidateEntriesByAuthor(glossary.id, siteId));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
@ -857,13 +819,10 @@ export class AddonModGlossaryProvider {
|
||||||
|
|
||||||
// Convenience function to store a new entry to be synchronized later.
|
// Convenience function to store a new entry to be synchronized later.
|
||||||
const storeOffline = async (): Promise<false> => {
|
const storeOffline = async (): Promise<false> => {
|
||||||
const discardTime = otherOptions.discardEntry?.timecreated;
|
|
||||||
|
|
||||||
if (otherOptions.checkDuplicates) {
|
if (otherOptions.checkDuplicates) {
|
||||||
// Check if the entry is duplicated in online or offline mode.
|
// Check if the entry is duplicated in online or offline mode.
|
||||||
const conceptUsed = await this.isConceptUsed(glossaryId, concept, {
|
const conceptUsed = await this.isConceptUsed(glossaryId, concept, {
|
||||||
cmId: otherOptions.cmId,
|
cmId: otherOptions.cmId,
|
||||||
timeCreated: discardTime,
|
|
||||||
siteId: otherOptions.siteId,
|
siteId: otherOptions.siteId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -877,17 +836,16 @@ export class AddonModGlossaryProvider {
|
||||||
throw new CoreError('Error adding entry.');
|
throw new CoreError('Error adding entry.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await AddonModGlossaryOffline.addNewEntry(
|
await AddonModGlossaryOffline.addOfflineEntry(
|
||||||
glossaryId,
|
glossaryId,
|
||||||
concept,
|
concept,
|
||||||
definition,
|
definition,
|
||||||
courseId,
|
courseId,
|
||||||
|
otherOptions.timeCreated ?? Date.now(),
|
||||||
entryOptions,
|
entryOptions,
|
||||||
attachments,
|
attachments,
|
||||||
otherOptions.timeCreated,
|
|
||||||
otherOptions.siteId,
|
otherOptions.siteId,
|
||||||
undefined,
|
undefined,
|
||||||
otherOptions.discardEntry,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -898,19 +856,9 @@ export class AddonModGlossaryProvider {
|
||||||
return storeOffline();
|
return storeOffline();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are editing an offline entry, discard previous first.
|
|
||||||
if (otherOptions.discardEntry) {
|
|
||||||
await AddonModGlossaryOffline.deleteNewEntry(
|
|
||||||
glossaryId,
|
|
||||||
otherOptions.discardEntry.concept,
|
|
||||||
otherOptions.discardEntry.timecreated,
|
|
||||||
otherOptions.siteId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to add it in online.
|
// Try to add it in online.
|
||||||
return await this.addEntryOnline(
|
const entryId = await this.addEntryOnline(
|
||||||
glossaryId,
|
glossaryId,
|
||||||
concept,
|
concept,
|
||||||
definition,
|
definition,
|
||||||
|
@ -918,6 +866,8 @@ export class AddonModGlossaryProvider {
|
||||||
<number> attachments,
|
<number> attachments,
|
||||||
otherOptions.siteId,
|
otherOptions.siteId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return entryId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (otherOptions.allowOffline && !CoreUtils.isWebServiceError(error)) {
|
if (otherOptions.allowOffline && !CoreUtils.isWebServiceError(error)) {
|
||||||
// Couldn't connect to server, store in offline.
|
// Couldn't connect to server, store in offline.
|
||||||
|
@ -959,7 +909,7 @@ export class AddonModGlossaryProvider {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (attachId) {
|
if (attachId) {
|
||||||
params.options!.push({
|
params.options?.push({
|
||||||
name: 'attachmentsid',
|
name: 'attachmentsid',
|
||||||
value: String(attachId),
|
value: String(attachId),
|
||||||
});
|
});
|
||||||
|
@ -967,9 +917,71 @@ export class AddonModGlossaryProvider {
|
||||||
|
|
||||||
const response = await site.write<AddonModGlossaryAddEntryWSResponse>('mod_glossary_add_entry', params);
|
const response = await site.write<AddonModGlossaryAddEntryWSResponse>('mod_glossary_add_entry', params);
|
||||||
|
|
||||||
|
CoreEvents.trigger(GLOSSARY_ENTRY_ADDED, { glossaryId, entryId: response.entryid }, siteId);
|
||||||
|
|
||||||
return response.entryid;
|
return response.entryid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing entry on a glossary.
|
||||||
|
*
|
||||||
|
* @param glossaryId Glossary ID.
|
||||||
|
* @param entryId Entry ID.
|
||||||
|
* @param concept Glossary entry concept.
|
||||||
|
* @param definition Glossary entry concept definition.
|
||||||
|
* @param options Options for the entry.
|
||||||
|
* @param attachId Attachments ID (if any attachment).
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
*/
|
||||||
|
async updateEntry(
|
||||||
|
glossaryId: number,
|
||||||
|
entryId: number,
|
||||||
|
concept: string,
|
||||||
|
definition: string,
|
||||||
|
options?: Record<string, AddonModGlossaryEntryOption>,
|
||||||
|
attachId?: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const params: AddonModGlossaryUpdateEntryWSParams = {
|
||||||
|
entryid: entryId,
|
||||||
|
concept: concept,
|
||||||
|
definition: definition,
|
||||||
|
definitionformat: 1,
|
||||||
|
options: CoreUtils.objectToArrayOfObjects(options || {}, 'name', 'value'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (attachId) {
|
||||||
|
params.options?.push({
|
||||||
|
name: 'attachmentsid',
|
||||||
|
value: String(attachId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await site.write<AddonModGlossaryUpdateEntryWSResponse>('mod_glossary_update_entry', params);
|
||||||
|
|
||||||
|
if (!response.result) {
|
||||||
|
throw new CoreError(response.warnings?.[0].message ?? 'Error updating entry');
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreEvents.trigger(GLOSSARY_ENTRY_UPDATED, { glossaryId, entryId }, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete entry.
|
||||||
|
*
|
||||||
|
* @param glossaryId Glossary id.
|
||||||
|
* @param entryId Entry id.
|
||||||
|
*/
|
||||||
|
async deleteEntry(glossaryId: number, entryId: number): Promise<void> {
|
||||||
|
const site = CoreSites.getRequiredCurrentSite();
|
||||||
|
|
||||||
|
await site.write('mod_glossary_delete_entry', { entryid: entryId });
|
||||||
|
|
||||||
|
CoreEvents.trigger(GLOSSARY_ENTRY_DELETED, { glossaryId, entryId });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a entry concept is already used.
|
* Check if a entry concept is already used.
|
||||||
*
|
*
|
||||||
|
@ -989,7 +1001,7 @@ export class AddonModGlossaryProvider {
|
||||||
|
|
||||||
// If we get here, there's no offline entry with this name, check online.
|
// If we get here, there's no offline entry with this name, check online.
|
||||||
// Get entries from the cache.
|
// Get entries from the cache.
|
||||||
const entries = await this.fetchAllEntries((options) => this.getEntriesByLetter(glossaryId, 'ALL', options), {
|
const entries = await this.fetchAllEntries((options) => this.getEntriesByLetter(glossaryId, options), {
|
||||||
cmId: options.cmId,
|
cmId: options.cmId,
|
||||||
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
|
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
|
||||||
siteId: options.siteId,
|
siteId: options.siteId,
|
||||||
|
@ -1010,15 +1022,14 @@ export class AddonModGlossaryProvider {
|
||||||
* @param mode The mode in which the glossary was viewed.
|
* @param mode The mode in which the glossary was viewed.
|
||||||
* @param name Name of the glossary.
|
* @param name Name of the glossary.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved when the WS call is successful.
|
|
||||||
*/
|
*/
|
||||||
logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise<void> {
|
async logView(glossaryId: number, mode: string, name?: string, siteId?: string): Promise<void> {
|
||||||
const params: AddonModGlossaryViewGlossaryWSParams = {
|
const params: AddonModGlossaryViewGlossaryWSParams = {
|
||||||
id: glossaryId,
|
id: glossaryId,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
return CoreCourseLogHelper.logSingle(
|
await CoreCourseLogHelper.logSingle(
|
||||||
'mod_glossary_view_glossary',
|
'mod_glossary_view_glossary',
|
||||||
params,
|
params,
|
||||||
AddonModGlossaryProvider.COMPONENT,
|
AddonModGlossaryProvider.COMPONENT,
|
||||||
|
@ -1037,14 +1048,13 @@ export class AddonModGlossaryProvider {
|
||||||
* @param glossaryId Glossary ID.
|
* @param glossaryId Glossary ID.
|
||||||
* @param name Name of the glossary.
|
* @param name Name of the glossary.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved when the WS call is successful.
|
|
||||||
*/
|
*/
|
||||||
logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise<void> {
|
async logEntryView(entryId: number, glossaryId: number, name?: string, siteId?: string): Promise<void> {
|
||||||
const params: AddonModGlossaryViewEntryWSParams = {
|
const params: AddonModGlossaryViewEntryWSParams = {
|
||||||
id: entryId,
|
id: entryId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return CoreCourseLogHelper.logSingle(
|
await CoreCourseLogHelper.logSingle(
|
||||||
'mod_glossary_view_entry',
|
'mod_glossary_view_entry',
|
||||||
params,
|
params,
|
||||||
AddonModGlossaryProvider.COMPONENT,
|
AddonModGlossaryProvider.COMPONENT,
|
||||||
|
@ -1063,7 +1073,6 @@ export class AddonModGlossaryProvider {
|
||||||
* @param entries Entries.
|
* @param entries Entries.
|
||||||
* @param from The "page" the entries belong to.
|
* @param from The "page" the entries belong to.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
*/
|
||||||
protected async storeEntries(
|
protected async storeEntries(
|
||||||
glossaryId: number,
|
glossaryId: number,
|
||||||
|
@ -1081,7 +1090,6 @@ export class AddonModGlossaryProvider {
|
||||||
* @param entryId Entry ID.
|
* @param entryId Entry ID.
|
||||||
* @param from The "page" the entry belongs to.
|
* @param from The "page" the entry belongs to.
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @returns Promise resolved when done.
|
|
||||||
*/
|
*/
|
||||||
protected async storeEntryId(glossaryId: number, entryId: number, from: number, siteId?: string): Promise<void> {
|
protected async storeEntryId(glossaryId: number, entryId: number, from: number, siteId?: string): Promise<void> {
|
||||||
const site = await CoreSites.getSite(siteId);
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
@ -1107,18 +1115,38 @@ declare module '@singletons/events' {
|
||||||
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||||
*/
|
*/
|
||||||
export interface CoreEventsData {
|
export interface CoreEventsData {
|
||||||
[AddonModGlossaryProvider.ADD_ENTRY_EVENT]: AddonModGlossaryAddEntryEventData;
|
[GLOSSARY_ENTRY_ADDED]: AddonModGlossaryEntryAddedEventData;
|
||||||
[AddonModGlossarySyncProvider.AUTO_SYNCED]: AddonModGlossaryAutoSyncData;
|
[GLOSSARY_ENTRY_UPDATED]: AddonModGlossaryEntryUpdatedEventData;
|
||||||
|
[GLOSSARY_ENTRY_DELETED]: AddonModGlossaryEntryDeletedEventData;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data passed to ADD_ENTRY_EVENT.
|
* GLOSSARY_ENTRY_ADDED event payload.
|
||||||
*/
|
*/
|
||||||
export type AddonModGlossaryAddEntryEventData = {
|
export type AddonModGlossaryEntryAddedEventData = {
|
||||||
glossaryId: number;
|
glossaryId: number;
|
||||||
entryId?: number;
|
entryId?: number;
|
||||||
|
timecreated?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GLOSSARY_ENTRY_UPDATED event payload.
|
||||||
|
*/
|
||||||
|
export type AddonModGlossaryEntryUpdatedEventData = {
|
||||||
|
glossaryId: number;
|
||||||
|
entryId?: number;
|
||||||
|
timecreated?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GLOSSARY_ENTRY_DELETED event payload.
|
||||||
|
*/
|
||||||
|
export type AddonModGlossaryEntryDeletedEventData = {
|
||||||
|
glossaryId: number;
|
||||||
|
entryId?: number;
|
||||||
|
timecreated?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1369,6 +1397,35 @@ export type AddonModGlossaryAddEntryWSResponse = {
|
||||||
warnings?: CoreWSExternalWarning[];
|
warnings?: CoreWSExternalWarning[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_glossary_update_entry WS.
|
||||||
|
*/
|
||||||
|
export type AddonModGlossaryUpdateEntryWSParams = {
|
||||||
|
entryid: number; // Glossary entry id to update.
|
||||||
|
concept: string; // Glossary concept.
|
||||||
|
definition: string; // Glossary concept definition.
|
||||||
|
definitionformat: number; // Definition format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||||
|
options?: { // Optional settings.
|
||||||
|
name: string; // The allowed keys (value format) are:
|
||||||
|
// inlineattachmentsid (int); the draft file area id for inline attachments
|
||||||
|
// attachmentsid (int); the draft file area id for attachments
|
||||||
|
// categories (comma separated int); comma separated category ids
|
||||||
|
// aliases (comma separated str); comma separated aliases
|
||||||
|
// usedynalink (bool); whether the entry should be automatically linked.
|
||||||
|
// casesensitive (bool); whether the entry is case sensitive.
|
||||||
|
// fullmatch (bool); whether to match whole words only.
|
||||||
|
value: string | number; // The value of the option (validated inside the function).
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by mod_glossary_update_entry WS.
|
||||||
|
*/
|
||||||
|
export type AddonModGlossaryUpdateEntryWSResponse = {
|
||||||
|
result: boolean; // The update result.
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Params of mod_glossary_view_glossary WS.
|
* Params of mod_glossary_view_glossary WS.
|
||||||
*/
|
*/
|
||||||
|
@ -1389,37 +1446,12 @@ export type AddonModGlossaryViewEntryWSParams = {
|
||||||
*/
|
*/
|
||||||
export type AddonModGlossaryAddEntryOptions = {
|
export type AddonModGlossaryAddEntryOptions = {
|
||||||
timeCreated?: number; // The time the entry was created. If not defined, current time.
|
timeCreated?: number; // The time the entry was created. If not defined, current time.
|
||||||
discardEntry?: AddonModGlossaryDiscardedEntry; // The entry provided will be discarded if found.
|
|
||||||
allowOffline?: boolean; // True if it can be stored in offline, false otherwise.
|
allowOffline?: boolean; // True if it can be stored in offline, false otherwise.
|
||||||
checkDuplicates?: boolean; // Check for duplicates before storing offline. Only used if allowOffline is true.
|
checkDuplicates?: boolean; // Check for duplicates before storing offline. Only used if allowOffline is true.
|
||||||
cmId?: number; // Module ID.
|
cmId?: number; // Module ID.
|
||||||
siteId?: string; // Site ID. If not defined, current site.
|
siteId?: string; // Site ID. If not defined, current site.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Entry to discard.
|
|
||||||
*/
|
|
||||||
export type AddonModGlossaryDiscardedEntry = {
|
|
||||||
concept: string;
|
|
||||||
timecreated: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entry to be added.
|
|
||||||
*/
|
|
||||||
export type AddonModGlossaryNewEntry = {
|
|
||||||
concept: string;
|
|
||||||
definition: string;
|
|
||||||
timecreated: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entry to be added, including attachments.
|
|
||||||
*/
|
|
||||||
export type AddonModGlossaryNewEntryWithFiles = AddonModGlossaryNewEntry & {
|
|
||||||
files: CoreFileEntry[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options to pass to the different get entries functions.
|
* Options to pass to the different get entries functions.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -51,14 +51,8 @@ export class AddonModGlossaryEditLinkHandlerService extends CoreContentLinksHand
|
||||||
);
|
);
|
||||||
|
|
||||||
await CoreNavigator.navigateToSitePath(
|
await CoreNavigator.navigateToSitePath(
|
||||||
AddonModGlossaryModuleHandlerService.PAGE_NAME + '/edit/0',
|
AddonModGlossaryModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/entry/new`,
|
||||||
{
|
{ siteId },
|
||||||
params: {
|
|
||||||
courseId: module.course,
|
|
||||||
cmId: module.id,
|
|
||||||
},
|
|
||||||
siteId,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingglossary', true);
|
||||||
|
|
|
@ -56,14 +56,8 @@ export class AddonModGlossaryEntryLinkHandlerService extends CoreContentLinksHan
|
||||||
);
|
);
|
||||||
|
|
||||||
await CoreNavigator.navigateToSitePath(
|
await CoreNavigator.navigateToSitePath(
|
||||||
AddonModGlossaryModuleHandlerService.PAGE_NAME + `/entry/${entryId}`,
|
AddonModGlossaryModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/entry/${entryId}`,
|
||||||
{
|
{ siteId },
|
||||||
params: {
|
|
||||||
courseId: module.course,
|
|
||||||
cmId: module.id,
|
|
||||||
},
|
|
||||||
siteId,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_glossary.errorloadingentry', true);
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr
|
||||||
const glossary = await AddonModGlossary.getGlossary(courseId, module.id);
|
const glossary = await AddonModGlossary.getGlossary(courseId, module.id);
|
||||||
|
|
||||||
const entries = await AddonModGlossary.fetchAllEntries(
|
const entries = await AddonModGlossary.fetchAllEntries(
|
||||||
(options) => AddonModGlossary.getEntriesByLetter(glossary.id, 'ALL', options),
|
(options) => AddonModGlossary.getEntriesByLetter(glossary.id, options),
|
||||||
{
|
{
|
||||||
cmId: module.id,
|
cmId: module.id,
|
||||||
},
|
},
|
||||||
|
@ -125,43 +125,23 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr
|
||||||
break;
|
break;
|
||||||
case 'cat':
|
case 'cat':
|
||||||
promises.push(AddonModGlossary.fetchAllEntries(
|
promises.push(AddonModGlossary.fetchAllEntries(
|
||||||
(newOptions) => AddonModGlossary.getEntriesByCategory(
|
(newOptions) => AddonModGlossary.getEntriesByCategory(glossary.id, newOptions),
|
||||||
glossary.id,
|
|
||||||
AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
|
|
||||||
newOptions,
|
|
||||||
),
|
|
||||||
options,
|
options,
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
case 'date':
|
case 'date':
|
||||||
promises.push(AddonModGlossary.fetchAllEntries(
|
promises.push(AddonModGlossary.fetchAllEntries(
|
||||||
(newOptions) => AddonModGlossary.getEntriesByDate(
|
(newOptions) => AddonModGlossary.getEntriesByDate(glossary.id, 'CREATION', newOptions),
|
||||||
glossary.id,
|
|
||||||
'CREATION',
|
|
||||||
'DESC',
|
|
||||||
newOptions,
|
|
||||||
),
|
|
||||||
options,
|
options,
|
||||||
));
|
));
|
||||||
promises.push(AddonModGlossary.fetchAllEntries(
|
promises.push(AddonModGlossary.fetchAllEntries(
|
||||||
(newOptions) => AddonModGlossary.getEntriesByDate(
|
(newOptions) => AddonModGlossary.getEntriesByDate(glossary.id, 'UPDATE', newOptions),
|
||||||
glossary.id,
|
|
||||||
'UPDATE',
|
|
||||||
'DESC',
|
|
||||||
newOptions,
|
|
||||||
),
|
|
||||||
options,
|
options,
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
case 'author':
|
case 'author':
|
||||||
promises.push(AddonModGlossary.fetchAllEntries(
|
promises.push(AddonModGlossary.fetchAllEntries(
|
||||||
(newOptions) => AddonModGlossary.getEntriesByAuthor(
|
(newOptions) => AddonModGlossary.getEntriesByAuthor(glossary.id, newOptions),
|
||||||
glossary.id,
|
|
||||||
'ALL',
|
|
||||||
'LASTNAME',
|
|
||||||
'ASC',
|
|
||||||
newOptions,
|
|
||||||
),
|
|
||||||
options,
|
options,
|
||||||
));
|
));
|
||||||
break;
|
break;
|
||||||
|
@ -171,7 +151,7 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr
|
||||||
|
|
||||||
// Fetch all entries to get information from.
|
// Fetch all entries to get information from.
|
||||||
promises.push(AddonModGlossary.fetchAllEntries(
|
promises.push(AddonModGlossary.fetchAllEntries(
|
||||||
(newOptions) => AddonModGlossary.getEntriesByLetter(glossary.id, 'ALL', newOptions),
|
(newOptions) => AddonModGlossary.getEntriesByLetter(glossary.id, newOptions),
|
||||||
options,
|
options,
|
||||||
).then((entries) => {
|
).then((entries) => {
|
||||||
const promises: Promise<unknown>[] = [];
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
|
@ -154,6 +154,152 @@ Feature: Test basic usage of glossary in app
|
||||||
Then I should find "Garlic" in the app
|
Then I should find "Garlic" in the app
|
||||||
And I should find "Allium sativum" in the app
|
And I should find "Allium sativum" in the app
|
||||||
|
|
||||||
|
Scenario: Edit entries
|
||||||
|
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
|
||||||
|
|
||||||
|
# Online
|
||||||
|
When I press "Cucumber" in the app
|
||||||
|
And I press "Edit entry" in the app
|
||||||
|
Then the field "Concept" matches value "Cucumber" in the app
|
||||||
|
And the field "Definition" matches value "Sweet cucumber" in the app
|
||||||
|
But I should not find "Keyword(s)" in the app
|
||||||
|
And I should not find "Categories" in the app
|
||||||
|
|
||||||
|
When I set the following fields to these values in the app:
|
||||||
|
| Concept | Coconut |
|
||||||
|
| Definition | Coconut is a fruit |
|
||||||
|
And I press "Add file" in the app
|
||||||
|
And I upload "stub1.txt" to "File" ".action-sheet-button" in the app
|
||||||
|
And I press "Add file" in the app
|
||||||
|
And I upload "stub2.txt" to "File" ".action-sheet-button" in the app
|
||||||
|
And I press "This entry should be automatically linked" "ion-toggle" in the app
|
||||||
|
And I press "This entry is case sensitive" "ion-toggle" in the app
|
||||||
|
And I press "Match whole words only" "ion-toggle" in the app
|
||||||
|
And I press "Save" in the app
|
||||||
|
Then I should find "Coconut is a fruit" in the app
|
||||||
|
And I should find "stub1.txt" in the app
|
||||||
|
And I should find "stub2.txt" in the app
|
||||||
|
But I should not find "Cucumber is a fruit" in the app
|
||||||
|
|
||||||
|
When I press "Edit entry" in the app
|
||||||
|
Then I should find "stub1.txt" in the app
|
||||||
|
And I should find "stub2.txt" in the app
|
||||||
|
And "This entry should be automatically linked" "ion-toggle" should be selected in the app
|
||||||
|
And "This entry is case sensitive" "ion-toggle" should be selected in the app
|
||||||
|
And "Match whole words only" "ion-toggle" should be selected in the app
|
||||||
|
|
||||||
|
When I press "Delete" within "stub2.txt" "ion-item" in the app
|
||||||
|
And I press "Delete" near "Are you sure you want to delete this file?" in the app
|
||||||
|
And I press "Add file" in the app
|
||||||
|
And I upload "stub3.txt" to "File" ".action-sheet-button" in the app
|
||||||
|
And I press "Save" in the app
|
||||||
|
Then I should find "stub1.txt" in the app
|
||||||
|
And I should find "stub3.txt" in the app
|
||||||
|
But I should not find "stub2.txt" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
Then I should find "Coconut" in the app
|
||||||
|
And I should find "Potato" in the app
|
||||||
|
But I should not find "Cucumber" in the app
|
||||||
|
|
||||||
|
# Offline
|
||||||
|
When I press "Add a new entry" in the app
|
||||||
|
And I switch network connection to offline
|
||||||
|
And I set the following fields to these values in the app:
|
||||||
|
| Concept | Broccoli |
|
||||||
|
| Definition | Brassica oleracea var. italica |
|
||||||
|
| Keyword(s) | vegetable, healthy |
|
||||||
|
And I press "Categories" in the app
|
||||||
|
And I press "The ones I like" in the app
|
||||||
|
And I press "OK" in the app
|
||||||
|
And I press "Add file" in the app
|
||||||
|
And I upload "stub1.txt" to "File" ".action-sheet-button" in the app
|
||||||
|
And I press "Add file" in the app
|
||||||
|
And I upload "stub2.txt" to "File" ".action-sheet-button" in the app
|
||||||
|
And I press "This entry should be automatically linked" "ion-toggle" in the app
|
||||||
|
And I press "This entry is case sensitive" "ion-toggle" in the app
|
||||||
|
And I press "Match whole words only" "ion-toggle" in the app
|
||||||
|
And I press "Save" in the app
|
||||||
|
Then I should find "Potato" in the app
|
||||||
|
And I should find "Broccoli" in the app
|
||||||
|
|
||||||
|
When I press "Broccoli" in the app
|
||||||
|
Then I should find "Brassica oleracea var. italica" in the app
|
||||||
|
And I should find "stub1.txt" in the app
|
||||||
|
And I should find "stub2.txt" in the app
|
||||||
|
|
||||||
|
When I press "Edit entry" in the app
|
||||||
|
Then the field "Concept" matches value "Broccoli" in the app
|
||||||
|
And the field "Definition" matches value "Brassica oleracea var. italica" in the app
|
||||||
|
And the field "Keyword(s)" matches value "vegetable, healthy" in the app
|
||||||
|
And I should find "The ones I like" in the app
|
||||||
|
And I should find "stub1.txt" in the app
|
||||||
|
And I should find "stub2.txt" in the app
|
||||||
|
And "This entry should be automatically linked" "ion-toggle" should be selected in the app
|
||||||
|
And "This entry is case sensitive" "ion-toggle" should be selected in the app
|
||||||
|
And "Match whole words only" "ion-toggle" should be selected in the app
|
||||||
|
|
||||||
|
When I set the following fields to these values in the app:
|
||||||
|
| Concept | Pickle |
|
||||||
|
| Definition | Pickle Rick |
|
||||||
|
And I press "Delete" within "stub2.txt" "ion-item" in the app
|
||||||
|
And I press "Delete" near "Are you sure you want to delete this file?" in the app
|
||||||
|
And I press "Add file" in the app
|
||||||
|
And I upload "stub3.txt" to "File" ".action-sheet-button" in the app
|
||||||
|
And I press "Save" in the app
|
||||||
|
Then I should find "Pickle Rick" in the app
|
||||||
|
And I should find "stub1.txt" in the app
|
||||||
|
And I should find "stub3.txt" in the app
|
||||||
|
But I should not find "stub2.txt" in the app
|
||||||
|
And I should not find "Brassica oleracea var. italica" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
Then I should find "Pickle" in the app
|
||||||
|
And I should find "Potato" in the app
|
||||||
|
But I should not find "Broccoli" in the app
|
||||||
|
|
||||||
|
When I switch network connection to wifi
|
||||||
|
And I press "Information" in the app
|
||||||
|
And I press "Synchronise now" in the app
|
||||||
|
Then I should not find "This Glossary has offline data to be synchronised" in the app
|
||||||
|
|
||||||
|
When I press "Pickle" in the app
|
||||||
|
Then I should find "Pickle Rick" in the app
|
||||||
|
And I should find "stub1.txt" in the app
|
||||||
|
And I should find "stub3.txt" in the app
|
||||||
|
But I should not find "stub2.txt" in the app
|
||||||
|
And I should not find "Brassica oleracea var. italica" in the app
|
||||||
|
|
||||||
|
Scenario: Delete entries
|
||||||
|
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
|
||||||
|
|
||||||
|
# Online
|
||||||
|
When I press "Cucumber" in the app
|
||||||
|
And I press "Delete entry" in the app
|
||||||
|
And I press "OK" near "Are you sure you want to delete this entry?" in the app
|
||||||
|
Then I should find "Entry deleted" in the app
|
||||||
|
And I should find "Potato" in the app
|
||||||
|
But I should not find "Cucumber" in the app
|
||||||
|
|
||||||
|
# Offline
|
||||||
|
When I press "Add a new entry" in the app
|
||||||
|
And I switch network connection to offline
|
||||||
|
And I set the following fields to these values in the app:
|
||||||
|
| Concept | Broccoli |
|
||||||
|
| Definition | Brassica oleracea var. italica |
|
||||||
|
And I press "Save" in the app
|
||||||
|
Then I should find "Potato" in the app
|
||||||
|
And I should find "Broccoli" in the app
|
||||||
|
|
||||||
|
When I press "Broccoli" in the app
|
||||||
|
Then I should find "Brassica oleracea var. italica" in the app
|
||||||
|
|
||||||
|
When I press "Delete entry" in the app
|
||||||
|
And I press "OK" near "Are you sure you want to delete this entry?" in the app
|
||||||
|
Then I should find "Entry deleted" in the app
|
||||||
|
And I should find "Potato" in the app
|
||||||
|
But I should not find "Broccoli" in the app
|
||||||
|
|
||||||
Scenario: Sync
|
Scenario: Sync
|
||||||
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
|
Given I entered the glossary activity "Test glossary" on course "Course 1" as "student1" in the app
|
||||||
And I press "Add a new entry" in the app
|
And I press "Add a new entry" in the app
|
||||||
|
@ -192,8 +338,8 @@ Feature: Test basic usage of glossary in app
|
||||||
And I should find "Broccoli" in the app
|
And I should find "Broccoli" in the app
|
||||||
And I should find "Cabbage" in the app
|
And I should find "Cabbage" in the app
|
||||||
And I should find "Garlic" in the app
|
And I should find "Garlic" in the app
|
||||||
But I should not see "Entries to be synced"
|
But I should not find "Entries to be synced" in the app
|
||||||
And I should not see "This Glossary has offline data to be synchronised."
|
And I should not find "This Glossary has offline data to be synchronised." in the app
|
||||||
|
|
||||||
When I press "Garlic" in the app
|
When I press "Garlic" in the app
|
||||||
Then I should find "Garlic" in the app
|
Then I should find "Garlic" in the app
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
This is a stub.
|
|
@ -0,0 +1 @@
|
||||||
|
This is a stub.
|
|
@ -0,0 +1 @@
|
||||||
|
This is a stub.
|
|
@ -200,6 +200,17 @@ Feature: Test glossary navigation
|
||||||
When I swipe to the left in the app
|
When I swipe to the left in the app
|
||||||
Then I should find "Acerola is a fruit" in the app
|
Then I should find "Acerola is a fruit" in the app
|
||||||
|
|
||||||
|
# Edit
|
||||||
|
When I swipe to the right in the app
|
||||||
|
And I press "Edit entry" in the app
|
||||||
|
And I press "Save" in the app
|
||||||
|
Then I should find "Tomato is a fruit" in the app
|
||||||
|
|
||||||
|
When I press the back button in the app
|
||||||
|
Then I should find "Tomato" in the app
|
||||||
|
And I should find "Cashew" in the app
|
||||||
|
And I should find "Acerola" in the app
|
||||||
|
|
||||||
@ci_jenkins_skip
|
@ci_jenkins_skip
|
||||||
Scenario: Tablet navigation on glossary
|
Scenario: Tablet navigation on glossary
|
||||||
Given I entered the course "Course 1" as "student1" in the app
|
Given I entered the course "Course 1" as "student1" in the app
|
||||||
|
@ -280,6 +291,7 @@ Feature: Test glossary navigation
|
||||||
| Concept | Tomato |
|
| Concept | Tomato |
|
||||||
| Definition | Tomato is a fruit |
|
| Definition | Tomato is a fruit |
|
||||||
And I press "Save" in the app
|
And I press "Save" in the app
|
||||||
|
And I press "Add a new entry" in the app
|
||||||
And I set the following fields to these values in the app:
|
And I set the following fields to these values in the app:
|
||||||
| Concept | Cashew |
|
| Concept | Cashew |
|
||||||
| Definition | Cashew is a fruit |
|
| Definition | Cashew is a fruit |
|
||||||
|
@ -300,3 +312,12 @@ Feature: Test glossary navigation
|
||||||
When I press "Acerola" in the app
|
When I press "Acerola" in the app
|
||||||
Then "Acerola" near "Tomato" should be selected in the app
|
Then "Acerola" near "Tomato" should be selected in the app
|
||||||
And I should find "Acerola is a fruit" inside the split-view content in the app
|
And I should find "Acerola is a fruit" inside the split-view content in the app
|
||||||
|
|
||||||
|
# Edit
|
||||||
|
When I press "Tomato" in the app
|
||||||
|
And I press "Edit entry" in the app
|
||||||
|
And I press "Save" in the app
|
||||||
|
Then I should find "Tomato is a fruit" inside the split-view content in the app
|
||||||
|
And I should find "Tomato" in the app
|
||||||
|
And I should find "Cashew" in the app
|
||||||
|
And I should find "Acerola" in the app
|
||||||
|
|
|
@ -1772,9 +1772,9 @@ export class CoreUtilsProvider {
|
||||||
* @param fallback Value to return if the promise is rejected.
|
* @param fallback Value to return if the promise is rejected.
|
||||||
* @returns Promise with ignored errors, resolving to the fallback result if provided.
|
* @returns Promise with ignored errors, resolving to the fallback result if provided.
|
||||||
*/
|
*/
|
||||||
async ignoreErrors<Result>(promise: Promise<Result>): Promise<Result | undefined>;
|
async ignoreErrors<Result>(promise?: Promise<Result>): Promise<Result | undefined>;
|
||||||
async ignoreErrors<Result, Fallback>(promise: Promise<Result>, fallback: Fallback): Promise<Result | Fallback>;
|
async ignoreErrors<Result, Fallback>(promise: Promise<Result>, fallback: Fallback): Promise<Result | Fallback>;
|
||||||
async ignoreErrors<Result, Fallback>(promise: Promise<Result>, fallback?: Fallback): Promise<Result | Fallback | undefined> {
|
async ignoreErrors<Result, Fallback>(promise?: Promise<Result>, fallback?: Fallback): Promise<Result | Fallback | undefined> {
|
||||||
try {
|
try {
|
||||||
const result = await promise;
|
const result = await promise;
|
||||||
|
|
||||||
|
|
|
@ -421,7 +421,7 @@ export class TestingBehatDomUtilsService {
|
||||||
*/
|
*/
|
||||||
findElementBasedOnText(
|
findElementBasedOnText(
|
||||||
locator: TestingBehatElementLocator,
|
locator: TestingBehatElementLocator,
|
||||||
options: TestingBehatFindOptions,
|
options: TestingBehatFindOptions = {},
|
||||||
): HTMLElement | undefined {
|
): HTMLElement | undefined {
|
||||||
return this.findElementsBasedOnText(locator, options)[0];
|
return this.findElementsBasedOnText(locator, options)[0];
|
||||||
}
|
}
|
||||||
|
@ -437,7 +437,7 @@ export class TestingBehatDomUtilsService {
|
||||||
locator: TestingBehatElementLocator,
|
locator: TestingBehatElementLocator,
|
||||||
options: TestingBehatFindOptions,
|
options: TestingBehatFindOptions,
|
||||||
): HTMLElement[] {
|
): HTMLElement[] {
|
||||||
const topContainers = this.getCurrentTopContainerElements(options.containerName);
|
const topContainers = this.getCurrentTopContainerElements(options.containerName ?? '');
|
||||||
let elements: HTMLElement[] = [];
|
let elements: HTMLElement[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < topContainers.length; i++) {
|
for (let i = 0; i < topContainers.length; i++) {
|
||||||
|
|
|
@ -361,6 +361,40 @@ export class TestingBehatRuntimeService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a file input id, adding it if necessary.
|
||||||
|
*
|
||||||
|
* @param locator Input locator.
|
||||||
|
* @returns Input id if successful, or ERROR: followed by message
|
||||||
|
*/
|
||||||
|
async getFileInputId(locator: TestingBehatElementLocator): Promise<string> {
|
||||||
|
this.log('Action - Upload File', { locator });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const inputOrContainer = TestingBehatDomUtils.findElementBasedOnText(locator);
|
||||||
|
|
||||||
|
if (!inputOrContainer) {
|
||||||
|
return 'ERROR: No element matches input locator.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = inputOrContainer.matches('input[type="file"]')
|
||||||
|
? inputOrContainer
|
||||||
|
: inputOrContainer.querySelector('input[type="file"]');
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
return 'ERROR: Input element does not contain a file input.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.hasAttribute('id')) {
|
||||||
|
input.setAttribute('id', `file-${Date.now()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.getAttribute('id') ?? '';
|
||||||
|
} catch (error) {
|
||||||
|
return 'ERROR: ' + error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a pull to refresh gesture in the current page.
|
* Trigger a pull to refresh gesture in the current page.
|
||||||
*
|
*
|
||||||
|
@ -635,8 +669,8 @@ export type BehatTestsWindow = Window & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestingBehatFindOptions = {
|
export type TestingBehatFindOptions = {
|
||||||
containerName: string;
|
containerName?: string;
|
||||||
onlyClickable: boolean;
|
onlyClickable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestingBehatElementLocator = {
|
export type TestingBehatElementLocator = {
|
||||||
|
|
|
@ -6,6 +6,7 @@ information provided here is intended especially for developers.
|
||||||
- CoreIconComponent has been removed after deprecation period: Use CoreFaIconDirective instead.
|
- CoreIconComponent has been removed after deprecation period: Use CoreFaIconDirective instead.
|
||||||
- The courseSummaryComponent property has been removed from the CoreCourseFormatComponent component, and the getCourseSummaryComponent method from the CoreCourseFormatHandler interface.
|
- The courseSummaryComponent property has been removed from the CoreCourseFormatComponent component, and the getCourseSummaryComponent method from the CoreCourseFormatHandler interface.
|
||||||
- Font Awesome icon library has been updated to 6.3.0.
|
- Font Awesome icon library has been updated to 6.3.0.
|
||||||
|
- Some methods in glossary addon services have changed.
|
||||||
|
|
||||||
=== 4.1.0 ===
|
=== 4.1.0 ===
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue