900 lines
31 KiB
JavaScript
900 lines
31 KiB
JavaScript
|
|
/**
|
||
|
|
* @typedef {SocketRequest} ManageCompendiumRequest
|
||
|
|
* @property {string} action The request action.
|
||
|
|
* @property {PackageCompendiumData|string} data The compendium creation data, or the ID of the compendium to delete.
|
||
|
|
* @property {object} [options] Additional options.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {SocketResponse} ManageCompendiumResponse
|
||
|
|
* @property {ManageCompendiumRequest} request The original request.
|
||
|
|
* @property {PackageCompendiumData|string} result The compendium creation data, or the collection name of the
|
||
|
|
* deleted compendium.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A collection of Document objects contained within a specific compendium pack.
|
||
|
|
* Each Compendium pack has its own associated instance of the CompendiumCollection class which contains its contents.
|
||
|
|
* @extends {DocumentCollection}
|
||
|
|
* @abstract
|
||
|
|
* @see {Game#packs}
|
||
|
|
*
|
||
|
|
* @param {object} metadata The compendium metadata, an object provided by game.data
|
||
|
|
*/
|
||
|
|
class CompendiumCollection extends DirectoryCollectionMixin(DocumentCollection) {
|
||
|
|
constructor(metadata) {
|
||
|
|
super([]);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The compendium metadata which defines the compendium content and location
|
||
|
|
* @type {object}
|
||
|
|
*/
|
||
|
|
this.metadata = metadata;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A subsidiary collection which contains the more minimal index of the pack
|
||
|
|
* @type {Collection<string, object>}
|
||
|
|
*/
|
||
|
|
this.index = new foundry.utils.Collection();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A subsidiary collection which contains the folders within the pack
|
||
|
|
* @type {Collection<string, Folder>}
|
||
|
|
*/
|
||
|
|
this.#folders = new CompendiumFolderCollection(this);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A debounced function which will clear the contents of the Compendium pack if it is not accessed frequently.
|
||
|
|
* @type {Function}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
this._flush = foundry.utils.debounce(this.clear.bind(this), this.constructor.CACHE_LIFETIME_SECONDS * 1000);
|
||
|
|
|
||
|
|
// Initialize a provided Compendium index
|
||
|
|
this.#indexedFields = new Set(this.documentClass.metadata.compendiumIndexFields);
|
||
|
|
for ( let i of metadata.index ) {
|
||
|
|
i.uuid = this.getUuid(i._id);
|
||
|
|
this.index.set(i._id, i);
|
||
|
|
}
|
||
|
|
delete metadata.index;
|
||
|
|
for ( let f of metadata.folders.sort((a, b) => a.sort - b.sort) ) {
|
||
|
|
this.#folders.set(f._id, new Folder.implementation(f, {pack: this.collection}));
|
||
|
|
}
|
||
|
|
delete metadata.folders;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The amount of time that Document instances within this CompendiumCollection are held in memory.
|
||
|
|
* Accessing the contents of the Compendium pack extends the duration of this lifetime.
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
static CACHE_LIFETIME_SECONDS = 300;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The named game setting which contains Compendium configurations.
|
||
|
|
* @type {string}
|
||
|
|
*/
|
||
|
|
static CONFIG_SETTING = "compendiumConfiguration";
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The canonical Compendium name - comprised of the originating package and the pack name
|
||
|
|
* @type {string}
|
||
|
|
*/
|
||
|
|
get collection() {
|
||
|
|
return this.metadata.id;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The banner image for this Compendium pack, or the default image for the pack type if no image is set.
|
||
|
|
* @returns {string|null|void}
|
||
|
|
*/
|
||
|
|
get banner() {
|
||
|
|
if ( this.metadata.banner === undefined ) return CONFIG[this.metadata.type]?.compendiumBanner;
|
||
|
|
return this.metadata.banner;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A reference to the Application class which provides an interface to interact with this compendium content.
|
||
|
|
* @type {typeof Application}
|
||
|
|
*/
|
||
|
|
applicationClass = Compendium;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The set of Compendium Folders
|
||
|
|
*/
|
||
|
|
#folders;
|
||
|
|
|
||
|
|
get folders() {
|
||
|
|
return this.#folders;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
get maxFolderDepth() {
|
||
|
|
return super.maxFolderDepth - 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the Folder that this Compendium is displayed within
|
||
|
|
* @returns {Folder|null}
|
||
|
|
*/
|
||
|
|
get folder() {
|
||
|
|
return game.folders.get(this.config.folder) ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Assign this CompendiumCollection to be organized within a specific Folder.
|
||
|
|
* @param {Folder|string|null} folder The desired Folder within the World or null to clear the folder
|
||
|
|
* @returns {Promise<void>} A promise which resolves once the transaction is complete
|
||
|
|
*/
|
||
|
|
async setFolder(folder) {
|
||
|
|
const current = this.config.folder;
|
||
|
|
|
||
|
|
// Clear folder
|
||
|
|
if ( folder === null ) {
|
||
|
|
if ( current === null ) return;
|
||
|
|
return this.configure({folder: null});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set folder
|
||
|
|
if ( typeof folder === "string" ) folder = game.folders.get(folder);
|
||
|
|
if ( !(folder instanceof Folder) ) throw new Error("You must pass a valid Folder or Folder ID.");
|
||
|
|
if ( folder.type !== "Compendium" ) throw new Error(`Folder "${folder.id}" is not of the required Compendium type`);
|
||
|
|
if ( folder.id === current ) return;
|
||
|
|
await this.configure({folder: folder.id});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the sort order for this Compendium
|
||
|
|
* @returns {number}
|
||
|
|
*/
|
||
|
|
get sort() {
|
||
|
|
return this.config.sort ?? 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_getVisibleTreeContents() {
|
||
|
|
return this.index.contents;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
static _sortStandard(a, b) {
|
||
|
|
return a.sort - b.sort;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Access the compendium configuration data for this pack
|
||
|
|
* @type {object}
|
||
|
|
*/
|
||
|
|
get config() {
|
||
|
|
const setting = game.settings.get("core", "compendiumConfiguration");
|
||
|
|
const config = setting[this.collection] || {};
|
||
|
|
/** @deprecated since v11 */
|
||
|
|
if ( "private" in config ) {
|
||
|
|
if ( config.private === true ) config.ownership = {PLAYER: "LIMITED", ASSISTANT: "OWNER"};
|
||
|
|
delete config.private;
|
||
|
|
}
|
||
|
|
return config;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
get documentName() {
|
||
|
|
return this.metadata.type;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Track whether the Compendium Collection is locked for editing
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
get locked() {
|
||
|
|
return this.config.locked ?? (this.metadata.packageType !== "world");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The visibility configuration of this compendium pack.
|
||
|
|
* @type {Record<CONST.USER_ROLES, CONST.DOCUMENT_OWNERSHIP_LEVELS>}
|
||
|
|
*/
|
||
|
|
get ownership() {
|
||
|
|
return this.config.ownership ?? this.metadata.ownership ?? {...Module.schema.getField("packs.ownership").initial};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Is this Compendium pack visible to the current game User?
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
get visible() {
|
||
|
|
return this.getUserLevel() >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A convenience reference to the label which should be used as the title for the Compendium pack.
|
||
|
|
* @type {string}
|
||
|
|
*/
|
||
|
|
get title() {
|
||
|
|
return this.metadata.label;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The index fields which should be loaded for this compendium pack
|
||
|
|
* @type {Set<string>}
|
||
|
|
*/
|
||
|
|
get indexFields() {
|
||
|
|
const coreFields = this.documentClass.metadata.compendiumIndexFields;
|
||
|
|
const configFields = CONFIG[this.documentName].compendiumIndexFields || [];
|
||
|
|
return new Set([...coreFields, ...configFields]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Track which document fields have been indexed for this compendium pack
|
||
|
|
* @type {Set<string>}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
#indexedFields;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Has this compendium pack been fully indexed?
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
get indexed() {
|
||
|
|
return this.indexFields.isSubset(this.#indexedFields);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Methods */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
get(key, options) {
|
||
|
|
this._flush();
|
||
|
|
return super.get(key, options);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
set(id, document) {
|
||
|
|
if ( document instanceof Folder ) {
|
||
|
|
return this.#folders.set(id, document);
|
||
|
|
}
|
||
|
|
this._flush();
|
||
|
|
this.indexDocument(document);
|
||
|
|
return super.set(id, document);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
delete(id) {
|
||
|
|
this.index.delete(id);
|
||
|
|
return super.delete(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritDoc */
|
||
|
|
clear() {
|
||
|
|
for ( const doc of this.values() ) {
|
||
|
|
if ( !Object.values(doc.apps).some(app => app.rendered) ) super.delete(doc.id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load the Compendium index and cache it as the keys and values of the Collection.
|
||
|
|
* @param {object} [options] Options which customize how the index is created
|
||
|
|
* @param {string[]} [options.fields] An array of fields to return as part of the index
|
||
|
|
* @returns {Promise<Collection>}
|
||
|
|
*/
|
||
|
|
async getIndex({fields=[]}={}) {
|
||
|
|
const cls = this.documentClass;
|
||
|
|
|
||
|
|
// Maybe reuse the existing index if we have already indexed all fields
|
||
|
|
const indexFields = new Set([...this.indexFields, ...fields]);
|
||
|
|
if ( indexFields.isSubset(this.#indexedFields) ) return this.index;
|
||
|
|
|
||
|
|
// Request the new index from the server
|
||
|
|
const index = await cls.database.get(cls, {
|
||
|
|
query: {},
|
||
|
|
index: true,
|
||
|
|
indexFields: Array.from(indexFields),
|
||
|
|
pack: this.collection
|
||
|
|
}, game.user);
|
||
|
|
|
||
|
|
// Assign the index to the collection
|
||
|
|
for ( let i of index ) {
|
||
|
|
const x = this.index.get(i._id);
|
||
|
|
const indexed = x ? foundry.utils.mergeObject(x, i) : i;
|
||
|
|
indexed.uuid = this.getUuid(indexed._id);
|
||
|
|
this.index.set(i._id, indexed);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Record that the pack has been indexed
|
||
|
|
console.log(`${vtt} | Constructed index of ${this.collection} Compendium containing ${this.index.size} entries`);
|
||
|
|
this.#indexedFields = indexFields;
|
||
|
|
return this.index;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get a single Document from this Compendium by ID.
|
||
|
|
* The document may already be locally cached, otherwise it is retrieved from the server.
|
||
|
|
* @param {string} id The requested Document id
|
||
|
|
* @returns {Promise<Document>|undefined} The retrieved Document instance
|
||
|
|
*/
|
||
|
|
async getDocument(id) {
|
||
|
|
if ( !id ) return undefined;
|
||
|
|
const cached = this.get(id);
|
||
|
|
if ( cached instanceof foundry.abstract.Document ) return cached;
|
||
|
|
const documents = await this.getDocuments({_id: id});
|
||
|
|
return documents.length ? documents.shift() : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load multiple documents from the Compendium pack using a provided query object.
|
||
|
|
* @param {object} query A database query used to retrieve documents from the underlying database
|
||
|
|
* @returns {Promise<Document[]>} The retrieved Document instances
|
||
|
|
*
|
||
|
|
* @example Get Documents that match the given value only.
|
||
|
|
* ```js
|
||
|
|
* await pack.getDocuments({ type: "weapon" });
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* @example Get several Documents by their IDs.
|
||
|
|
* ```js
|
||
|
|
* await pack.getDocuments({ _id__in: arrayOfIds });
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* @example Get Documents by their sub-types.
|
||
|
|
* ```js
|
||
|
|
* await pack.getDocuments({ type__in: ["weapon", "armor"] });
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
async getDocuments(query={}) {
|
||
|
|
const cls = this.documentClass;
|
||
|
|
const documents = await cls.database.get(cls, {query, pack: this.collection}, game.user);
|
||
|
|
for ( let d of documents ) {
|
||
|
|
if ( d.invalid && !this.invalidDocumentIds.has(d.id) ) {
|
||
|
|
this.invalidDocumentIds.add(d.id);
|
||
|
|
this._source.push(d);
|
||
|
|
}
|
||
|
|
else this.set(d.id, d);
|
||
|
|
}
|
||
|
|
return documents;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the ownership level that a User has for this Compendium pack.
|
||
|
|
* @param {documents.User} user The user being tested
|
||
|
|
* @returns {number} The ownership level in CONST.DOCUMENT_OWNERSHIP_LEVELS
|
||
|
|
*/
|
||
|
|
getUserLevel(user=game.user) {
|
||
|
|
const levels = CONST.DOCUMENT_OWNERSHIP_LEVELS;
|
||
|
|
let level = levels.NONE;
|
||
|
|
for ( const [role, l] of Object.entries(this.ownership) ) {
|
||
|
|
if ( user.hasRole(role) ) level = Math.max(level, levels[l]);
|
||
|
|
}
|
||
|
|
return level;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Test whether a certain User has a requested permission level (or greater) over the Compendium pack
|
||
|
|
* @param {documents.BaseUser} user The User being tested
|
||
|
|
* @param {string|number} permission The permission level from DOCUMENT_OWNERSHIP_LEVELS to test
|
||
|
|
* @param {object} options Additional options involved in the permission test
|
||
|
|
* @param {boolean} [options.exact=false] Require the exact permission level requested?
|
||
|
|
* @returns {boolean} Does the user have this permission level over the Compendium pack?
|
||
|
|
*/
|
||
|
|
testUserPermission(user, permission, {exact=false}={}) {
|
||
|
|
const perms = CONST.DOCUMENT_OWNERSHIP_LEVELS;
|
||
|
|
const level = user.isGM ? perms.OWNER : this.getUserLevel(user);
|
||
|
|
const target = (typeof permission === "string") ? (perms[permission] ?? perms.OWNER) : permission;
|
||
|
|
return exact ? level === target : level >= target;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Import a Document into this Compendium Collection.
|
||
|
|
* @param {Document} document The existing Document you wish to import
|
||
|
|
* @param {object} [options] Additional options which modify how the data is imported.
|
||
|
|
* See {@link ClientDocumentMixin#toCompendium}
|
||
|
|
* @returns {Promise<Document>} The imported Document instance
|
||
|
|
*/
|
||
|
|
async importDocument(document, options={}) {
|
||
|
|
if ( !(document instanceof this.documentClass) && !(document instanceof Folder) ) {
|
||
|
|
const err = Error(`You may not import a ${document.constructor.name} Document into the ${this.collection} Compendium which contains ${this.documentClass.name} Documents.`);
|
||
|
|
ui.notifications.error(err.message);
|
||
|
|
throw err;
|
||
|
|
}
|
||
|
|
options.clearOwnership = options.clearOwnership ?? (this.metadata.packageType === "world");
|
||
|
|
const data = document.toCompendium(this, options);
|
||
|
|
|
||
|
|
return document.constructor.create(data, {pack: this.collection});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Import a Folder into this Compendium Collection.
|
||
|
|
* @param {Folder} folder The existing Folder you wish to import
|
||
|
|
* @param {object} [options] Additional options which modify how the data is imported.
|
||
|
|
* @param {boolean} [options.importParents=true] Import any parent folders which are not already present in the Compendium
|
||
|
|
* @returns {Promise<void>}
|
||
|
|
*/
|
||
|
|
async importFolder(folder, {importParents=true, ...options}={}) {
|
||
|
|
if ( !(folder instanceof Folder) ) {
|
||
|
|
const err = Error(`You may not import a ${folder.constructor.name} Document into the folders collection of the ${this.collection} Compendium.`);
|
||
|
|
ui.notifications.error(err.message);
|
||
|
|
throw err;
|
||
|
|
}
|
||
|
|
|
||
|
|
const toCreate = [folder];
|
||
|
|
if ( importParents ) toCreate.push(...folder.getParentFolders().filter(f => !this.folders.has(f.id)));
|
||
|
|
await Folder.createDocuments(toCreate, {pack: this.collection, keepId: true});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Import an array of Folders into this Compendium Collection.
|
||
|
|
* @param {Folder[]} folders The existing Folders you wish to import
|
||
|
|
* @param {object} [options] Additional options which modify how the data is imported.
|
||
|
|
* @param {boolean} [options.importParents=true] Import any parent folders which are not already present in the Compendium
|
||
|
|
* @returns {Promise<void>}
|
||
|
|
*/
|
||
|
|
async importFolders(folders, {importParents=true, ...options}={}) {
|
||
|
|
if ( folders.some(f => !(f instanceof Folder)) ) {
|
||
|
|
const err = Error(`You can only import Folder documents into the folders collection of the ${this.collection} Compendium.`);
|
||
|
|
ui.notifications.error(err.message);
|
||
|
|
throw err;
|
||
|
|
}
|
||
|
|
|
||
|
|
const toCreate = new Set(folders);
|
||
|
|
if ( importParents ) {
|
||
|
|
for ( const f of folders ) {
|
||
|
|
for ( const p of f.getParentFolders() ) {
|
||
|
|
if ( !this.folders.has(p.id) ) toCreate.add(p);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
await Folder.createDocuments(Array.from(toCreate), {pack: this.collection, keepId: true});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fully import the contents of a Compendium pack into a World folder.
|
||
|
|
* @param {object} [options={}] Options which modify the import operation. Additional options are forwarded to
|
||
|
|
* {@link WorldCollection#fromCompendium} and {@link Document.createDocuments}
|
||
|
|
* @param {string|null} [options.folderId] An existing Folder _id to use.
|
||
|
|
* @param {string} [options.folderName] A new Folder name to create.
|
||
|
|
* @returns {Promise<Document[]>} The imported Documents, now existing within the World
|
||
|
|
*/
|
||
|
|
async importAll({folderId=null, folderName="", ...options}={}) {
|
||
|
|
let parentFolder;
|
||
|
|
|
||
|
|
// Optionally, create a top level folder
|
||
|
|
if ( CONST.FOLDER_DOCUMENT_TYPES.includes(this.documentName) ) {
|
||
|
|
|
||
|
|
// Re-use an existing folder
|
||
|
|
if ( folderId ) parentFolder = game.folders.get(folderId, {strict: true});
|
||
|
|
|
||
|
|
// Create a new Folder
|
||
|
|
if ( !parentFolder ) {
|
||
|
|
parentFolder = await Folder.create({
|
||
|
|
name: folderName || this.title,
|
||
|
|
type: this.documentName,
|
||
|
|
parent: null,
|
||
|
|
color: this.folder?.color ?? null
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load all content
|
||
|
|
const folders = this.folders;
|
||
|
|
const documents = await this.getDocuments();
|
||
|
|
ui.notifications.info(game.i18n.format("COMPENDIUM.ImportAllStart", {
|
||
|
|
number: documents.length,
|
||
|
|
folderNumber: folders.size,
|
||
|
|
type: game.i18n.localize(this.documentClass.metadata.label),
|
||
|
|
folder: parentFolder.name
|
||
|
|
}));
|
||
|
|
|
||
|
|
// Create any missing Folders
|
||
|
|
const folderCreateData = folders.map(f => {
|
||
|
|
if ( game.folders.has(f.id) ) return null;
|
||
|
|
const data = f.toObject();
|
||
|
|
|
||
|
|
// If this folder has no parent folder, assign it to the new folder
|
||
|
|
if ( !data.folder ) data.folder = parentFolder.id;
|
||
|
|
return data;
|
||
|
|
}).filter(f => f);
|
||
|
|
await Folder.createDocuments(folderCreateData, {keepId: true});
|
||
|
|
|
||
|
|
// Prepare import data
|
||
|
|
const collection = game.collections.get(this.documentName);
|
||
|
|
const createData = documents.map(doc => {
|
||
|
|
const data = collection.fromCompendium(doc, options);
|
||
|
|
|
||
|
|
// If this document has no folder, assign it to the new folder
|
||
|
|
if ( !data.folder) data.folder = parentFolder.id;
|
||
|
|
return data;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create World Documents in batches
|
||
|
|
const chunkSize = 100;
|
||
|
|
const nBatches = Math.ceil(createData.length / chunkSize);
|
||
|
|
let created = [];
|
||
|
|
for ( let n=0; n<nBatches; n++ ) {
|
||
|
|
const chunk = createData.slice(n*chunkSize, (n+1)*chunkSize);
|
||
|
|
const docs = await this.documentClass.createDocuments(chunk, options);
|
||
|
|
created = created.concat(docs);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Notify of success
|
||
|
|
ui.notifications.info(game.i18n.format("COMPENDIUM.ImportAllFinish", {
|
||
|
|
number: created.length,
|
||
|
|
folderNumber: folders.size,
|
||
|
|
type: game.i18n.localize(this.documentClass.metadata.label),
|
||
|
|
folder: parentFolder.name
|
||
|
|
}));
|
||
|
|
return created;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Provide a dialog form that prompts the user to import the full contents of a Compendium pack into the World.
|
||
|
|
* @param {object} [options={}] Additional options passed to the Dialog.confirm method
|
||
|
|
* @returns {Promise<Document[]|boolean|null>} A promise which resolves in the following ways: an array of imported
|
||
|
|
* Documents if the "yes" button was pressed, false if the "no" button was pressed, or
|
||
|
|
* null if the dialog was closed without making a choice.
|
||
|
|
*/
|
||
|
|
async importDialog(options={}) {
|
||
|
|
|
||
|
|
// Render the HTML form
|
||
|
|
const collection = CONFIG[this.documentName]?.collection?.instance;
|
||
|
|
const html = await renderTemplate("templates/sidebar/apps/compendium-import.html", {
|
||
|
|
folderName: this.title,
|
||
|
|
keepId: options.keepId ?? false,
|
||
|
|
folders: collection?._formatFolderSelectOptions() ?? []
|
||
|
|
});
|
||
|
|
|
||
|
|
// Present the Dialog
|
||
|
|
options.jQuery = false;
|
||
|
|
return Dialog.confirm({
|
||
|
|
title: `${game.i18n.localize("COMPENDIUM.ImportAll")}: ${this.title}`,
|
||
|
|
content: html,
|
||
|
|
render: html => {
|
||
|
|
const form = html.querySelector("form");
|
||
|
|
form.elements.folder.addEventListener("change", event => {
|
||
|
|
form.elements.folderName.disabled = !!event.currentTarget.value;
|
||
|
|
}, { passive: true });
|
||
|
|
},
|
||
|
|
yes: html => {
|
||
|
|
const form = html.querySelector("form");
|
||
|
|
return this.importAll({
|
||
|
|
folderId: form.elements.folder.value,
|
||
|
|
folderName: form.folderName.value,
|
||
|
|
keepId: form.keepId.checked
|
||
|
|
});
|
||
|
|
},
|
||
|
|
options
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add a Document to the index, capturing its relevant index attributes
|
||
|
|
* @param {Document} document The document to index
|
||
|
|
*/
|
||
|
|
indexDocument(document) {
|
||
|
|
let index = this.index.get(document.id);
|
||
|
|
const data = document.toObject();
|
||
|
|
if ( index ) foundry.utils.mergeObject(index, data, {insertKeys: false, insertValues: false});
|
||
|
|
else {
|
||
|
|
index = this.#indexedFields.reduce((obj, field) => {
|
||
|
|
foundry.utils.setProperty(obj, field, foundry.utils.getProperty(data, field));
|
||
|
|
return obj;
|
||
|
|
}, {});
|
||
|
|
}
|
||
|
|
index.img = data.thumb ?? data.img;
|
||
|
|
index._id = data._id;
|
||
|
|
index.uuid = document.uuid;
|
||
|
|
this.index.set(document.id, index);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Prompt the gamemaster with a dialog to configure ownership of this Compendium pack.
|
||
|
|
* @returns {Promise<Record<string, string>>} The configured ownership for the pack
|
||
|
|
*/
|
||
|
|
async configureOwnershipDialog() {
|
||
|
|
if ( !game.user.isGM ) throw new Error("You do not have permission to configure ownership for this Compendium pack");
|
||
|
|
const current = this.ownership;
|
||
|
|
const levels = {
|
||
|
|
"": game.i18n.localize("COMPENDIUM.OwnershipInheritBelow"),
|
||
|
|
NONE: game.i18n.localize("OWNERSHIP.NONE"),
|
||
|
|
LIMITED: game.i18n.localize("OWNERSHIP.LIMITED"),
|
||
|
|
OBSERVER: game.i18n.localize("OWNERSHIP.OBSERVER"),
|
||
|
|
OWNER: game.i18n.localize("OWNERSHIP.OWNER")
|
||
|
|
};
|
||
|
|
const roles = {
|
||
|
|
ASSISTANT: {label: "USER.RoleAssistant", value: current.ASSISTANT, levels: { ...levels }},
|
||
|
|
TRUSTED: {label: "USER.RoleTrusted", value: current.TRUSTED, levels: { ...levels }},
|
||
|
|
PLAYER: {label: "USER.RolePlayer", value: current.PLAYER, levels: { ...levels }}
|
||
|
|
};
|
||
|
|
delete roles.PLAYER.levels[""];
|
||
|
|
await Dialog.wait({
|
||
|
|
title: `${game.i18n.localize("OWNERSHIP.Title")}: ${this.metadata.label}`,
|
||
|
|
content: await renderTemplate("templates/sidebar/apps/compendium-ownership.hbs", {roles}),
|
||
|
|
default: "ok",
|
||
|
|
close: () => null,
|
||
|
|
buttons: {
|
||
|
|
reset: {
|
||
|
|
label: game.i18n.localize("COMPENDIUM.OwnershipReset"),
|
||
|
|
icon: '<i class="fas fa-undo"></i>',
|
||
|
|
callback: () => this.configure({ ownership: undefined })
|
||
|
|
},
|
||
|
|
ok: {
|
||
|
|
label: game.i18n.localize("OWNERSHIP.Configure"),
|
||
|
|
icon: '<i class="fas fa-check"></i>',
|
||
|
|
callback: async html => {
|
||
|
|
const fd = new FormDataExtended(html.querySelector("form.compendium-ownership-dialog"));
|
||
|
|
let ownership = Object.entries(fd.object).reduce((obj, [r, l]) => {
|
||
|
|
if ( l ) obj[r] = l;
|
||
|
|
return obj;
|
||
|
|
}, {});
|
||
|
|
ownership.GAMEMASTER = "OWNER";
|
||
|
|
await this.configure({ownership});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, { jQuery: false });
|
||
|
|
return this.ownership;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Compendium Management */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Activate the Socket event listeners used to receive responses to compendium management events.
|
||
|
|
* @param {Socket} socket The active game socket.
|
||
|
|
* @internal
|
||
|
|
*/
|
||
|
|
static _activateSocketListeners(socket) {
|
||
|
|
socket.on("manageCompendium", response => {
|
||
|
|
const { request } = response;
|
||
|
|
switch ( request.action ) {
|
||
|
|
case "create":
|
||
|
|
CompendiumCollection.#handleCreateCompendium(response);
|
||
|
|
break;
|
||
|
|
case "delete":
|
||
|
|
CompendiumCollection.#handleDeleteCompendium(response);
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
throw new Error(`Invalid Compendium modification action ${request.action} provided.`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a new Compendium Collection using provided metadata.
|
||
|
|
* @param {object} metadata The compendium metadata used to create the new pack
|
||
|
|
* @param {object} options Additional options which modify the Compendium creation request
|
||
|
|
* @returns {Promise<CompendiumCollection>}
|
||
|
|
*/
|
||
|
|
static async createCompendium(metadata, options={}) {
|
||
|
|
if ( !game.user.isGM ) return ui.notifications.error("You do not have permission to modify this compendium pack");
|
||
|
|
const response = await SocketInterface.dispatch("manageCompendium", {
|
||
|
|
action: "create",
|
||
|
|
data: metadata,
|
||
|
|
options: options
|
||
|
|
});
|
||
|
|
|
||
|
|
return this.#handleCreateCompendium(response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a UUID for a given primary document ID within this Compendium pack
|
||
|
|
* @param {string} id The document ID to generate a UUID for
|
||
|
|
* @returns {string} The generated UUID, in the form of "Compendium.<collection>.<documentName>.<id>"
|
||
|
|
*/
|
||
|
|
getUuid(id) {
|
||
|
|
return `Compendium.${this.collection}.${this.documentName}.${id}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ----------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Assign configuration metadata settings to the compendium pack
|
||
|
|
* @param {object} configuration The object of compendium settings to define
|
||
|
|
* @returns {Promise} A Promise which resolves once the setting is updated
|
||
|
|
*/
|
||
|
|
configure(configuration={}) {
|
||
|
|
const settings = game.settings.get("core", "compendiumConfiguration");
|
||
|
|
const config = this.config;
|
||
|
|
for ( const [k, v] of Object.entries(configuration) ) {
|
||
|
|
if ( v === undefined ) delete config[k];
|
||
|
|
else config[k] = v;
|
||
|
|
}
|
||
|
|
settings[this.collection] = config;
|
||
|
|
return game.settings.set("core", this.constructor.CONFIG_SETTING, settings);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ----------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete an existing world-level Compendium Collection.
|
||
|
|
* This action may only be performed for world-level packs by a Gamemaster User.
|
||
|
|
* @returns {Promise<CompendiumCollection>}
|
||
|
|
*/
|
||
|
|
async deleteCompendium() {
|
||
|
|
this.#assertUserCanManage();
|
||
|
|
this.apps.forEach(app => app.close());
|
||
|
|
const response = await SocketInterface.dispatch("manageCompendium", {
|
||
|
|
action: "delete",
|
||
|
|
data: this.metadata.name
|
||
|
|
});
|
||
|
|
|
||
|
|
return CompendiumCollection.#handleDeleteCompendium(response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ----------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Duplicate a compendium pack to the current World.
|
||
|
|
* @param {string} label A new Compendium label
|
||
|
|
* @returns {Promise<CompendiumCollection>}
|
||
|
|
*/
|
||
|
|
async duplicateCompendium({label}={}) {
|
||
|
|
this.#assertUserCanManage({requireUnlocked: false});
|
||
|
|
label = label || this.title;
|
||
|
|
const metadata = foundry.utils.mergeObject(this.metadata, {
|
||
|
|
name: label.slugify({strict: true}),
|
||
|
|
label: label
|
||
|
|
}, {inplace: false});
|
||
|
|
return this.constructor.createCompendium(metadata, {source: this.collection});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ----------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate that the current user is able to modify content of this Compendium pack
|
||
|
|
* @returns {boolean}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
#assertUserCanManage({requireUnlocked=true}={}) {
|
||
|
|
const config = this.config;
|
||
|
|
let err;
|
||
|
|
if ( !game.user.isGM ) err = new Error("You do not have permission to modify this compendium pack");
|
||
|
|
if ( requireUnlocked && config.locked ) {
|
||
|
|
err = new Error("You cannot modify content in this compendium pack because it is locked.");
|
||
|
|
}
|
||
|
|
if ( err ) {
|
||
|
|
ui.notifications.error(err.message);
|
||
|
|
throw err;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Migrate a compendium pack.
|
||
|
|
* This operation re-saves all documents within the compendium pack to disk, applying the current data model.
|
||
|
|
* If the document type has system data, the latest system data template will also be applied to all documents.
|
||
|
|
* @returns {Promise<CompendiumCollection>}
|
||
|
|
*/
|
||
|
|
async migrate() {
|
||
|
|
this.#assertUserCanManage();
|
||
|
|
ui.notifications.info(`Beginning migration for Compendium pack ${this.collection}, please be patient.`);
|
||
|
|
await SocketInterface.dispatch("manageCompendium", {
|
||
|
|
type: this.collection,
|
||
|
|
action: "migrate",
|
||
|
|
data: this.collection,
|
||
|
|
options: { broadcast: false }
|
||
|
|
});
|
||
|
|
ui.notifications.info(`Successfully migrated Compendium pack ${this.collection}.`);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
async updateAll(transformation, condition=null, options={}) {
|
||
|
|
await this.getDocuments();
|
||
|
|
options.pack = this.collection;
|
||
|
|
return super.updateAll(transformation, condition, options);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Event Handlers */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritDoc */
|
||
|
|
_onModifyContents(action, documents, result, operation, user) {
|
||
|
|
super._onModifyContents(action, documents, result, operation, user);
|
||
|
|
Hooks.callAll("updateCompendium", this, documents, operation, user.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle a response from the server where a compendium was created.
|
||
|
|
* @param {ManageCompendiumResponse} response The server response.
|
||
|
|
* @returns {CompendiumCollection}
|
||
|
|
*/
|
||
|
|
static #handleCreateCompendium({ result }) {
|
||
|
|
game.data.packs.push(result);
|
||
|
|
const pack = new this(result);
|
||
|
|
game.packs.set(pack.collection, pack);
|
||
|
|
pack.apps.push(new Compendium({collection: pack}));
|
||
|
|
ui.compendium.render();
|
||
|
|
return pack;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle a response from the server where a compendium was deleted.
|
||
|
|
* @param {ManageCompendiumResponse} response The server response.
|
||
|
|
* @returns {CompendiumCollection}
|
||
|
|
*/
|
||
|
|
static #handleDeleteCompendium({ result }) {
|
||
|
|
const pack = game.packs.get(result);
|
||
|
|
if ( !pack ) throw new Error(`Compendium pack '${result}' did not exist to be deleted.`);
|
||
|
|
game.data.packs.findSplice(p => p.id === result);
|
||
|
|
game.packs.delete(result);
|
||
|
|
ui.compendium.render();
|
||
|
|
return pack;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Deprecations and Compatibility */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v11
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
get private() {
|
||
|
|
foundry.utils.logCompatibilityWarning("CompendiumCollection#private is deprecated in favor of the new "
|
||
|
|
+ "CompendiumCollection#ownership, CompendiumCollection#getUserLevel, CompendiumCollection#visible properties");
|
||
|
|
return !this.visible;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v11
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
get isOpen() {
|
||
|
|
foundry.utils.logCompatibilityWarning("CompendiumCollection#isOpen is deprecated and will be removed in V13");
|
||
|
|
return this.apps.some(app => app._state > Application.RENDER_STATES.NONE);
|
||
|
|
}
|
||
|
|
}
|