mirror of
https://github.com/agregarr/agregarr.git
synced 2026-01-26 04:18:52 -06:00
fix: existing sort title not being preserved for pre-existing collections
This commit is contained in:
@@ -3221,7 +3221,7 @@ components:
|
||||
securitySchemes:
|
||||
cookieAuth:
|
||||
type: apiKey
|
||||
name: connect.sid
|
||||
name: agregarr.sid
|
||||
in: cookie
|
||||
apiKey:
|
||||
type: apiKey
|
||||
|
||||
@@ -938,14 +938,30 @@ export abstract class BaseCollectionSync implements CollectionSyncInterface {
|
||||
);
|
||||
}
|
||||
|
||||
// Update sort title if needed
|
||||
if (sortOrderLibrary !== undefined) {
|
||||
// Update sort title if needed - for Agregarr-created collections
|
||||
// Find the config to check everLibraryPromoted status
|
||||
const settings = getSettings();
|
||||
const allConfigs = settings.plex.collectionConfigs || [];
|
||||
const matchingConfig = allConfigs.find((config) => {
|
||||
const configLibraryId = Array.isArray(config.libraryId)
|
||||
? config.libraryId[0]
|
||||
: config.libraryId;
|
||||
return (
|
||||
configLibraryId === options.libraryKey &&
|
||||
config.collectionRatingKey === collectionRatingKey
|
||||
);
|
||||
});
|
||||
|
||||
// Only update sortTitle if everLibraryPromoted is not explicitly false
|
||||
if (
|
||||
sortOrderLibrary !== undefined &&
|
||||
matchingConfig?.everLibraryPromoted !== false
|
||||
) {
|
||||
let sortTitle: string;
|
||||
const updateConfig: Partial<CollectionConfig> = {};
|
||||
|
||||
if (isLibraryPromoted && sortOrderLibrary > 0) {
|
||||
// Promoted collections get exclamation marks for manual positioning
|
||||
const settings = getSettings();
|
||||
const allConfigs = settings.plex.collectionConfigs || [];
|
||||
// Promoted: Set exclamation marks
|
||||
const sameLibraryConfigs = allConfigs.filter((config) => {
|
||||
const configLibraryId = Array.isArray(config.libraryId)
|
||||
? config.libraryId[0]
|
||||
@@ -969,14 +985,21 @@ export abstract class BaseCollectionSync implements CollectionSyncInterface {
|
||||
sortTitle = `!!${collectionName}`;
|
||||
}
|
||||
} else {
|
||||
// A-Z collections use natural title for alphabetical sorting
|
||||
// Demoted: Reset to natural title and mark as cleaned
|
||||
sortTitle = collectionName;
|
||||
// After reset, set everLibraryPromoted back to false
|
||||
updateConfig.everLibraryPromoted = false;
|
||||
}
|
||||
|
||||
await plexClient.updateCollectionSortTitle(
|
||||
collectionRatingKey,
|
||||
sortTitle
|
||||
);
|
||||
|
||||
// Update config if everLibraryPromoted needs to be reset
|
||||
if (updateConfig.everLibraryPromoted !== undefined && matchingConfig) {
|
||||
this.updateCollectionConfigField(matchingConfig.id, updateConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// Update visibility settings
|
||||
@@ -1045,6 +1068,44 @@ export abstract class BaseCollectionSync implements CollectionSyncInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update specific fields of a collection config
|
||||
*/
|
||||
private updateCollectionConfigField(
|
||||
configId: string,
|
||||
updateConfig: Partial<CollectionConfig>
|
||||
): void {
|
||||
try {
|
||||
const settings = getSettings();
|
||||
const collectionConfigs = settings.plex.collectionConfigs || [];
|
||||
const configIndex = collectionConfigs.findIndex((c) => c.id === configId);
|
||||
|
||||
if (configIndex !== -1) {
|
||||
collectionConfigs[configIndex] = {
|
||||
...collectionConfigs[configIndex],
|
||||
...updateConfig,
|
||||
};
|
||||
settings.plex.collectionConfigs = collectionConfigs;
|
||||
settings.save();
|
||||
|
||||
logger.debug(`Updated collection config fields: ${configId}`, {
|
||||
label: `${this.source} Collections`,
|
||||
configId,
|
||||
updatedFields: Object.keys(updateConfig),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to update collection config fields for ${configId}`,
|
||||
{
|
||||
label: `${this.source} Collections`,
|
||||
configId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Abstract methods that must be implemented by subclasses
|
||||
|
||||
/**
|
||||
|
||||
@@ -473,7 +473,7 @@ export class HubSyncService {
|
||||
type: 'collection',
|
||||
libraryId,
|
||||
collectionRatingKey: ratingKeyForLibrary,
|
||||
sortOrder: config.sortOrderHome || 0,
|
||||
sortOrder: config.sortOrderHome || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -494,7 +494,7 @@ export class HubSyncService {
|
||||
for (const [libraryId, libraryHubConfigs] of hubConfigsByLibrary) {
|
||||
// Sort hub configs by their sortOrderHome (this is our UI order for home/recommended)
|
||||
const sortedHubConfigs = [...libraryHubConfigs].sort(
|
||||
(a, b) => (a.sortOrderHome || 0) - (b.sortOrderHome || 0)
|
||||
(a, b) => (a.sortOrderHome || 1) - (b.sortOrderHome || 1)
|
||||
);
|
||||
|
||||
// Add hubs to ordering in UI order
|
||||
@@ -522,7 +522,7 @@ export class HubSyncService {
|
||||
type: 'hub',
|
||||
libraryId: hubConfig.libraryId,
|
||||
hubIdentifier: hubConfig.hubIdentifier,
|
||||
sortOrder: hubConfig.sortOrderHome || 0,
|
||||
sortOrder: hubConfig.sortOrderHome || 1,
|
||||
});
|
||||
} else {
|
||||
logger.warn(
|
||||
@@ -559,7 +559,7 @@ export class HubSyncService {
|
||||
for (const [libraryId, libraryConfigs] of configsByLibrary) {
|
||||
// Sort configs by their sortOrderHome (this is our UI order for home/recommended)
|
||||
const sortedConfigs = [...libraryConfigs].sort(
|
||||
(a, b) => (a.sortOrderHome || 0) - (b.sortOrderHome || 0)
|
||||
(a, b) => (a.sortOrderHome || 1) - (b.sortOrderHome || 1)
|
||||
);
|
||||
|
||||
// Add pre-existing collections to ordering in UI order
|
||||
@@ -586,7 +586,7 @@ export class HubSyncService {
|
||||
type: 'collection',
|
||||
libraryId: config.libraryId,
|
||||
collectionRatingKey: config.collectionRatingKey,
|
||||
sortOrder: config.sortOrderHome || 0,
|
||||
sortOrder: config.sortOrderHome || 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -710,15 +710,17 @@ export class HubSyncService {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only update sortTitle if sortOrderLibrary is defined
|
||||
if (config.sortOrderLibrary === undefined) {
|
||||
// Only update sortTitle if everLibraryPromoted is not explicitly false
|
||||
if (config.everLibraryPromoted === false) {
|
||||
// If everLibraryPromoted is explicitly false: DO NOT touch sortTitle at all
|
||||
continue;
|
||||
}
|
||||
|
||||
let sortTitle: string;
|
||||
const updateConfig: Partial<PreExistingCollectionConfig> = {};
|
||||
|
||||
if (config.isLibraryPromoted && config.sortOrderLibrary > 0) {
|
||||
// Promoted pre-existing collections get exclamation marks
|
||||
// Promoted: Set exclamation marks
|
||||
const sameLibraryConfigs = preExistingConfigs.filter(
|
||||
(c) =>
|
||||
c.libraryId === config.libraryId &&
|
||||
@@ -738,8 +740,10 @@ export class HubSyncService {
|
||||
sortTitle = `!!${config.name}`;
|
||||
}
|
||||
} else {
|
||||
// A-Z pre-existing collections use natural title for alphabetical sorting
|
||||
// Demoted: Reset to natural title and mark as cleaned
|
||||
sortTitle = config.name;
|
||||
// After reset, set everLibraryPromoted back to false
|
||||
updateConfig.everLibraryPromoted = false;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -748,6 +752,11 @@ export class HubSyncService {
|
||||
sortTitle
|
||||
);
|
||||
|
||||
// Update config if everLibraryPromoted needs to be reset
|
||||
if (updateConfig.everLibraryPromoted !== undefined) {
|
||||
this.updatePreExistingConfigField(config.id, updateConfig);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Updated sortTitle for pre-existing collection ${config.name}: ${sortTitle}`,
|
||||
{
|
||||
@@ -893,6 +902,50 @@ export class HubSyncService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update specific fields of a pre-existing collection config
|
||||
*/
|
||||
private updatePreExistingConfigField(
|
||||
configId: string,
|
||||
updateConfig: Partial<PreExistingCollectionConfig>
|
||||
): void {
|
||||
try {
|
||||
const settings = getSettings();
|
||||
const preExistingConfigs =
|
||||
settings.plex.preExistingCollectionConfigs || [];
|
||||
const configIndex = preExistingConfigs.findIndex(
|
||||
(c) => c.id === configId
|
||||
);
|
||||
|
||||
if (configIndex !== -1) {
|
||||
preExistingConfigs[configIndex] = {
|
||||
...preExistingConfigs[configIndex],
|
||||
...updateConfig,
|
||||
};
|
||||
settings.plex.preExistingCollectionConfigs = preExistingConfigs;
|
||||
settings.save();
|
||||
|
||||
logger.debug(
|
||||
`Updated pre-existing collection config fields: ${configId}`,
|
||||
{
|
||||
label: 'Hub Sync Service',
|
||||
configId,
|
||||
updatedFields: Object.keys(updateConfig),
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to update pre-existing collection config fields for ${configId}`,
|
||||
{
|
||||
label: 'Hub Sync Service',
|
||||
configId,
|
||||
error: extractErrorMessage(error),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default HubSyncService;
|
||||
|
||||
@@ -67,7 +67,7 @@ export class PreExistingCollectionConfigService {
|
||||
libraryId: newConfig.libraryId,
|
||||
libraryName: newConfig.libraryName,
|
||||
mediaType: newConfig.mediaType,
|
||||
sortOrderHome: newConfig.sortOrderHome || 0,
|
||||
sortOrderHome: newConfig.sortOrderHome || 1,
|
||||
sortOrderLibrary: newConfig.sortOrderLibrary || 0,
|
||||
isLibraryPromoted:
|
||||
newConfig.isLibraryPromoted ??
|
||||
@@ -210,7 +210,7 @@ export class PreExistingCollectionConfigService {
|
||||
libraryId: config.libraryId,
|
||||
libraryName: config.libraryName,
|
||||
mediaType: config.mediaType,
|
||||
sortOrderHome: config.sortOrderHome || 0,
|
||||
sortOrderHome: config.sortOrderHome || 1,
|
||||
sortOrderLibrary: config.sortOrderLibrary || 0,
|
||||
isLibraryPromoted:
|
||||
config.isLibraryPromoted ??
|
||||
|
||||
@@ -441,8 +441,14 @@ export function createHubConfigFromDiscovery(
|
||||
libraryName: library.title,
|
||||
mediaType,
|
||||
sortOrderLibrary: sortOrder.library,
|
||||
sortOrderHome: sortOrder.home,
|
||||
sortOrderHome:
|
||||
hubData.promotedToSharedHome ||
|
||||
hubData.promotedToOwnHome ||
|
||||
hubData.promotedToRecommended
|
||||
? sortOrder.home
|
||||
: 0, // 0 for void if not visible on any home/recommended screen
|
||||
isLibraryPromoted: false, // Hubs start in A-Z section (though they use different ordering logic)
|
||||
everLibraryPromoted: false, // Default: false for all discovered hubs
|
||||
collectionType: categorization.collectionType,
|
||||
visibilityConfig: {
|
||||
usersHome: hubData.promotedToSharedHome || false,
|
||||
@@ -490,8 +496,14 @@ export function createPreExistingConfigFromDiscovery(
|
||||
mediaType: detectedMediaType,
|
||||
titleSort: collectionData.titleSort, // Preserve titleSort for alphabetical sorting
|
||||
sortOrderLibrary: 0, // All discovered collections start in A-Z section with sortOrderLibrary: 0
|
||||
sortOrderHome: sortOrder.home,
|
||||
sortOrderHome:
|
||||
collectionData.promotedToSharedHome ||
|
||||
collectionData.promotedToOwnHome ||
|
||||
collectionData.promotedToRecommended
|
||||
? sortOrder.home
|
||||
: 0, // 0 for void if not visible on any home/recommended screen
|
||||
isLibraryPromoted: false, // All discovered collections start in A-Z section
|
||||
everLibraryPromoted: false, // Default: false for all discovered collections
|
||||
collectionType: CollectionType.PRE_EXISTING,
|
||||
visibilityConfig: {
|
||||
usersHome: collectionData.promotedToSharedHome || false,
|
||||
|
||||
@@ -45,12 +45,13 @@ export interface CollectionConfig {
|
||||
readonly customDays?: number; // Number of days for Tautulli collections (required for Tautulli type)
|
||||
readonly libraryId: string; // Library ID this collection belongs to
|
||||
readonly libraryName: string; // Library name for display
|
||||
readonly sortOrderHome?: number; // Order for Plex home screen (creation time based)
|
||||
readonly sortOrderHome?: number; // Order for Plex home screen (1+ for positioned items, 0 for void/unpositioned)
|
||||
readonly sortOrderLibrary?: number; // Order for Plex library tab (0 for A-Z section, 1+ for promoted section)
|
||||
readonly isLibraryPromoted?: boolean; // true = promoted section (uses exclamation marks), false = A-Z section (defaults to true for Agregarr collections)
|
||||
readonly isLinked?: boolean; // True if collection is actively linked to other collections
|
||||
readonly linkId?: number; // Group ID for linked collections (preserved even when isLinked=false)
|
||||
readonly isUnlinked?: boolean; // True if this collection was deliberately unlinked and should not be grouped with siblings
|
||||
everLibraryPromoted?: boolean; // True if this collection has ever been promoted to the promoted section (once true, stays true until sortTitle reset)
|
||||
readonly collectionRatingKey?: string; // Plex collection rating key (when created)
|
||||
// Custom URL fields for external collections
|
||||
readonly tmdbCustomCollectionUrl?: string;
|
||||
@@ -116,7 +117,7 @@ export interface PlexHubConfig {
|
||||
libraryId: string; // Library ID this hub belongs to
|
||||
libraryName: string; // Library display name
|
||||
mediaType: 'movie' | 'tv'; // Media type (hubs are always single type)
|
||||
sortOrderHome: number; // Position on Plex home screen
|
||||
sortOrderHome: number; // Position on Plex home screen (1+ for positioned items, 0 for void)
|
||||
sortOrderLibrary: number; // Position in library (0 for A-Z section, 1+ for promoted section)
|
||||
isLibraryPromoted: boolean; // true = promoted section (uses exclamation marks), false = A-Z section
|
||||
visibilityConfig: {
|
||||
@@ -135,6 +136,7 @@ export interface PlexHubConfig {
|
||||
isLinked?: boolean; // True if hub is actively linked to other hubs (set by backend linking logic)
|
||||
linkId?: number; // Group ID for linked hubs (set by backend linking logic)
|
||||
isUnlinked?: boolean; // True if this hub was deliberately unlinked and should not be grouped with siblings
|
||||
everLibraryPromoted?: boolean; // True if this hub has ever been promoted to the promoted section (once true, stays true until sortTitle reset)
|
||||
// Time restriction settings - all hub types can have time restrictions
|
||||
timeRestriction?: {
|
||||
readonly alwaysActive: boolean; // If true, hub is always active (default)
|
||||
@@ -171,7 +173,7 @@ export interface PreExistingCollectionConfig {
|
||||
libraryName: string; // Library display name
|
||||
mediaType: 'movie' | 'tv'; // Media type based on library type
|
||||
titleSort?: string; // Plex sortTitle field for alphabetical ordering
|
||||
sortOrderHome: number; // Position on Plex home screen
|
||||
sortOrderHome: number; // Position on Plex home screen (1+ for positioned items, 0 for void)
|
||||
sortOrderLibrary: number; // Position in library (0 for A-Z section, 1+ for promoted section)
|
||||
isLibraryPromoted: boolean; // true = promoted section (uses exclamation marks), false = A-Z section
|
||||
visibilityConfig: {
|
||||
@@ -190,6 +192,7 @@ export interface PreExistingCollectionConfig {
|
||||
isLinked?: boolean; // True if collection is actively linked to other collections (set by backend linking logic)
|
||||
linkId?: number; // Group ID for linked collections (set by backend linking logic)
|
||||
isUnlinked?: boolean; // True if this collection was deliberately unlinked and should not be grouped with siblings
|
||||
everLibraryPromoted?: boolean; // True if this collection has ever been promoted to the promoted section (once true, stays true until sortTitle reset)
|
||||
// Time restriction settings
|
||||
readonly timeRestriction?: {
|
||||
readonly alwaysActive: boolean; // If true, collection is always active (default)
|
||||
|
||||
@@ -424,7 +424,7 @@ collectionsRoutes.delete('/:id', isAuthenticated(), async (req, res) => {
|
||||
configsByLibrary.forEach((libraryConfigs) => {
|
||||
// Sort by current sortOrderHome to maintain relative order
|
||||
libraryConfigs.sort(
|
||||
(a, b) => (a.sortOrderHome || 0) - (b.sortOrderHome || 0)
|
||||
(a, b) => (a.sortOrderHome || 1) - (b.sortOrderHome || 1)
|
||||
);
|
||||
|
||||
// Reassign sequential sort orders
|
||||
@@ -590,8 +590,18 @@ collectionsRoutes.post('/create', isAuthenticated(), async (req, res) => {
|
||||
statType: req.body.tautulliStatType,
|
||||
subtype: req.body.subtype,
|
||||
};
|
||||
// For custom templates, choose the appropriate template based on library type
|
||||
let templateToProcess = req.body.template || req.body.name || '';
|
||||
if (req.body.template === 'custom') {
|
||||
if (libraryMediaType === 'movie' && req.body.customMovieTemplate) {
|
||||
templateToProcess = req.body.customMovieTemplate;
|
||||
} else if (libraryMediaType === 'tv' && req.body.customTVTemplate) {
|
||||
templateToProcess = req.body.customTVTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
let processedName = templateEngine.processTemplate(
|
||||
req.body.template || req.body.name || '',
|
||||
templateToProcess,
|
||||
context
|
||||
);
|
||||
|
||||
@@ -628,6 +638,7 @@ collectionsRoutes.post('/create', isAuthenticated(), async (req, res) => {
|
||||
isLinked: libraryIds.length > 1,
|
||||
linkId: linkId,
|
||||
isLibraryPromoted: true, // All new Agregarr collections start in promoted section
|
||||
everLibraryPromoted: true, // New collections start promoted, so mark as ever promoted
|
||||
// Remove multi-library fields that don't belong in individual configs
|
||||
libraryIds: undefined,
|
||||
libraryNames: undefined,
|
||||
|
||||
@@ -265,6 +265,7 @@ preExistingRoutes.patch('/:id/promote', isAuthenticated(), async (req, res) => {
|
||||
const finalConfig = preExistingCollectionConfigService.updateSettings(id, {
|
||||
isLibraryPromoted: true,
|
||||
sortOrderLibrary: maxSortOrder + 1,
|
||||
everLibraryPromoted: true, // Mark as ever promoted when promoting
|
||||
});
|
||||
|
||||
// Mark pre-existing collection as needing sync due to promotion
|
||||
@@ -320,10 +321,11 @@ preExistingRoutes.patch('/:id/demote', isAuthenticated(), async (req, res) => {
|
||||
.json({ error: 'Collection is already in A-Z section' });
|
||||
}
|
||||
|
||||
// Update in service
|
||||
// Update in service - Keep everLibraryPromoted: true when demoting
|
||||
const finalConfig = preExistingCollectionConfigService.updateSettings(id, {
|
||||
isLibraryPromoted: false,
|
||||
sortOrderLibrary: 0, // A-Z collections have sortOrderLibrary: 0
|
||||
// Note: everLibraryPromoted stays true when demoting - will be reset to false during sync after sortTitle cleanup
|
||||
});
|
||||
|
||||
// Mark pre-existing collection as needing sync due to demotion
|
||||
|
||||
@@ -200,18 +200,33 @@ async function handleManualReordering(
|
||||
// Strip metadata and add sort order
|
||||
const { configType, ...originalConfig } = item;
|
||||
|
||||
// Calculate sort order with special handling for promoted collections in Library context
|
||||
// Apply correct sort order logic based on context and collection type
|
||||
let sortOrder = index;
|
||||
if (
|
||||
sortOrderField === 'sortOrderLibrary' &&
|
||||
originalConfig.isLibraryPromoted
|
||||
) {
|
||||
// Promoted collections in Library context need to start from 1, not 0
|
||||
|
||||
// For Library context, respect the A-Z vs Promoted section design
|
||||
if (sortOrderField === 'sortOrderLibrary') {
|
||||
if (originalConfig.isLibraryPromoted === false) {
|
||||
sortOrder = 0; // A-Z section always gets 0
|
||||
} else {
|
||||
sortOrder = index + 1; // Promoted section starts from 1
|
||||
}
|
||||
} else {
|
||||
// For Home/Recommended contexts, start from 1 (0 is void value)
|
||||
sortOrder = index + 1;
|
||||
}
|
||||
|
||||
const updatedConfig = { ...originalConfig, [sortOrderField]: sortOrder };
|
||||
|
||||
// Set everLibraryPromoted: true when a collection is assigned to the promoted library section
|
||||
if (
|
||||
sortOrderField === 'sortOrderLibrary' &&
|
||||
originalConfig.isLibraryPromoted === true &&
|
||||
sortOrder > 0 &&
|
||||
configType === 'collection'
|
||||
) {
|
||||
updatedConfig.everLibraryPromoted = true;
|
||||
}
|
||||
|
||||
// Use type guards to ensure config matches declared type
|
||||
if (configType === 'collection' && isCollectionConfig(updatedConfig)) {
|
||||
collectionsToUpdate.push(updatedConfig);
|
||||
@@ -464,11 +479,41 @@ async function performAutoReordering(
|
||||
return (aSortOrder as number) - (bSortOrder as number);
|
||||
});
|
||||
|
||||
// Assign sequential sort orders (0, 1, 2, 3...)
|
||||
const updatedItems = allLibraryItems.map((item, index) => ({
|
||||
...item,
|
||||
[sortOrderField]: index,
|
||||
}));
|
||||
// Assign sequential sort orders with proper A-Z vs Promoted section logic
|
||||
const updatedItems = allLibraryItems.map((item, index) => {
|
||||
let newSortOrder = index;
|
||||
|
||||
// For Library context, respect the A-Z vs Promoted section design:
|
||||
// - A-Z section (isLibraryPromoted: false) → sortOrderLibrary: 0
|
||||
// - Promoted section (isLibraryPromoted: true) → sortOrderLibrary: 1, 2, 3...
|
||||
if (sortOrderField === 'sortOrderLibrary') {
|
||||
if (item.isLibraryPromoted === false) {
|
||||
newSortOrder = 0; // A-Z section always gets 0
|
||||
} else {
|
||||
newSortOrder = index + 1; // Promoted section starts from 1
|
||||
}
|
||||
} else {
|
||||
// For Home/Recommended contexts, start from 1 (0 is void value)
|
||||
newSortOrder = index + 1;
|
||||
}
|
||||
|
||||
const updatedItem = {
|
||||
...item,
|
||||
[sortOrderField]: newSortOrder,
|
||||
};
|
||||
|
||||
// Set everLibraryPromoted: true when a collection is assigned to the promoted library section
|
||||
if (
|
||||
sortOrderField === 'sortOrderLibrary' &&
|
||||
item.isLibraryPromoted === true &&
|
||||
newSortOrder > 0 &&
|
||||
item.configType === 'collection'
|
||||
) {
|
||||
updatedItem.everLibraryPromoted = true;
|
||||
}
|
||||
|
||||
return updatedItem;
|
||||
});
|
||||
|
||||
// Apply updates back to their respective services
|
||||
let totalUpdated = 0;
|
||||
|
||||
@@ -1372,12 +1372,16 @@ const CollectionFormConfigForm = ({
|
||||
libraryId: values.libraryId as string,
|
||||
libraryName: values.libraryName as string,
|
||||
name: generateCollectionName(values as CollectionFormConfig),
|
||||
// For custom templates, send the actual custom text as the template
|
||||
template:
|
||||
// For custom templates, pass both custom templates and let backend choose
|
||||
template: values.template,
|
||||
customMovieTemplate:
|
||||
values.template === 'custom'
|
||||
? (values as CollectionFormConfig).customMovieTemplate ||
|
||||
(values as CollectionFormConfig).customTVTemplate
|
||||
: values.template,
|
||||
? (values as CollectionFormConfig).customMovieTemplate
|
||||
: undefined,
|
||||
customTVTemplate:
|
||||
values.template === 'custom'
|
||||
? (values as CollectionFormConfig).customTVTemplate
|
||||
: undefined,
|
||||
// Convert string numbers to integers
|
||||
customDays: values.customDays
|
||||
? parseInt(values.customDays.toString(), 10)
|
||||
|
||||
@@ -534,8 +534,8 @@ const CollectionSettings = ({
|
||||
maxItems: 30,
|
||||
libraryId: '', // Start with no selection to show "Select Libraries..."
|
||||
libraryName: '',
|
||||
sortOrderHome: 0, // Default to top of home screen
|
||||
sortOrderLibrary: 0, // Default to top of library tab
|
||||
sortOrderHome: 1, // Default positioned item (0 is void)
|
||||
sortOrderLibrary: 1, // Default promoted section (0 is A-Z)
|
||||
customDays: 30, // Default for Tautulli collections
|
||||
tautulliStatType: 'plays', // Default stat type
|
||||
searchMissingMovies: false,
|
||||
|
||||
@@ -154,7 +154,7 @@ export const useCollectionEdit = () => {
|
||||
: hubConfig.mediaType === 'tv'
|
||||
? 'tv'
|
||||
: 'both',
|
||||
sortOrderHome: hubConfig.sortOrderHome || 0,
|
||||
sortOrderHome: hubConfig.sortOrderHome || 1,
|
||||
sortOrderLibrary: hubConfig.sortOrderLibrary,
|
||||
visibilityConfig: hubConfig.visibilityConfig,
|
||||
isDefaultPlexHub: hubConfig.isDefaultPlexHub,
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface PlexHubConfig {
|
||||
isLinked?: boolean; // True if hub is actively linked to other hubs (set by backend linking logic)
|
||||
linkId?: number; // Group ID for linked hubs (set by backend linking logic)
|
||||
isUnlinked?: boolean; // True if this hub was deliberately unlinked and should not be grouped with siblings
|
||||
everLibraryPromoted?: boolean; // True if this hub has ever been promoted to the promoted section (once true, stays true until sortTitle reset)
|
||||
// Time restriction settings - all hub types can have time restrictions
|
||||
timeRestriction?: {
|
||||
alwaysActive: boolean; // If true, hub is always active (default)
|
||||
@@ -87,6 +88,7 @@ export interface PreExistingCollectionConfig {
|
||||
isLinked?: boolean; // True if collection is actively linked to other collections (set by backend linking logic)
|
||||
linkId?: number; // Group ID for linked collections (set by backend linking logic)
|
||||
isUnlinked?: boolean; // True if this collection was deliberately unlinked and should not be grouped with siblings
|
||||
everLibraryPromoted?: boolean; // True if this collection has ever been promoted to the promoted section (once true, stays true until sortTitle reset)
|
||||
// Time restriction settings
|
||||
timeRestriction?: {
|
||||
alwaysActive: boolean; // If true, collection is always active (default)
|
||||
@@ -217,6 +219,7 @@ export interface CollectionFormConfig {
|
||||
// Backend properties (from PlexHubConfig) - Present on hub configs from API
|
||||
readonly collectionType?: CollectionType; // Simplified categorization system
|
||||
readonly isUnlinked?: boolean; // True if this hub was deliberately unlinked
|
||||
everLibraryPromoted?: boolean; // True if this collection has ever been promoted to the promoted section (once true, stays true until sortTitle reset)
|
||||
|
||||
// Hub-specific properties (present when config represents a hub)
|
||||
readonly hubIdentifier?: string; // Plex hub identifier (e.g., "movie.recentlyadded")
|
||||
|
||||
@@ -47,11 +47,11 @@ export function groupConfigsByLibrary(
|
||||
let bSortOrder: number;
|
||||
|
||||
if (activeTab === 'library') {
|
||||
aSortOrder = a.sortOrderLibrary ?? 0;
|
||||
bSortOrder = b.sortOrderLibrary ?? 0;
|
||||
aSortOrder = a.sortOrderLibrary ?? 0; // Keep 0 for A-Z section
|
||||
bSortOrder = b.sortOrderLibrary ?? 0; // Keep 0 for A-Z section
|
||||
} else {
|
||||
aSortOrder = a.sortOrderHome ?? 0;
|
||||
bSortOrder = b.sortOrderHome ?? 0;
|
||||
aSortOrder = a.sortOrderHome ?? 1; // 1+ for positioned, 0 for void
|
||||
bSortOrder = b.sortOrderHome ?? 1; // 1+ for positioned, 0 for void
|
||||
}
|
||||
|
||||
return aSortOrder - bSortOrder;
|
||||
@@ -112,8 +112,8 @@ export function normalizeConfigsForStorage(
|
||||
const normalized = configs.map((config) => ({
|
||||
...config,
|
||||
// Ensure sort orders are properly set
|
||||
sortOrderHome: config.sortOrderHome ?? 0,
|
||||
sortOrderLibrary: config.sortOrderLibrary ?? 0,
|
||||
sortOrderHome: config.sortOrderHome ?? 1, // 1+ for positioned, 0 for void
|
||||
sortOrderLibrary: config.sortOrderLibrary ?? 0, // Keep 0 for A-Z section
|
||||
}));
|
||||
|
||||
return normalized;
|
||||
|
||||
Reference in New Issue
Block a user