mirror of
https://github.com/agregarr/agregarr.git
synced 2026-01-24 11:28:55 -06:00
fix(linking collections): fixes name being propgated across all hubs when linking
adds tooltip to show which hubs will be linked/unlinked
This commit is contained in:
@@ -18,6 +18,33 @@ export class DefaultHubConfigService {
|
||||
const settings = getSettings();
|
||||
const hubConfigs = settings.plex.hubConfigs || [];
|
||||
|
||||
// Check if any hubs are missing linkIds (legacy hubs from before automatic linking was implemented)
|
||||
const hasHubsWithoutLinkIds = hubConfigs.some(
|
||||
(hub) => hub.linkId === undefined && hubConfigs.length > 1
|
||||
);
|
||||
|
||||
// If we have multiple hubs and some are missing linkIds, apply automatic linking and save
|
||||
if (hasHubsWithoutLinkIds) {
|
||||
logger.info(
|
||||
'Detected hubs without linkIds - applying automatic linking to repair legacy hubs',
|
||||
{
|
||||
label: 'Default Hub Config Service',
|
||||
totalHubs: hubConfigs.length,
|
||||
}
|
||||
);
|
||||
|
||||
const linkedConfigs = this.applyAutomaticLinking(hubConfigs);
|
||||
settings.plex.hubConfigs = linkedConfigs;
|
||||
settings.save();
|
||||
|
||||
logger.info('Automatic linking applied to legacy hubs', {
|
||||
label: 'Default Hub Config Service',
|
||||
linkedGroups: this.countLinkedGroups(linkedConfigs),
|
||||
});
|
||||
|
||||
return linkedConfigs;
|
||||
}
|
||||
|
||||
return hubConfigs;
|
||||
}
|
||||
|
||||
@@ -28,6 +55,7 @@ export class DefaultHubConfigService {
|
||||
const settings = getSettings();
|
||||
|
||||
// Preserve existing isActive status when updating hub configs
|
||||
// Also repairs any broken names from the linking bug (names are refreshed from Plex discovery data)
|
||||
const existingHubConfigs = settings.plex.hubConfigs || [];
|
||||
const mergedHubConfigs = newConfigs.map(
|
||||
(newConfig: DiscoveredHubConfig) => {
|
||||
@@ -36,12 +64,30 @@ export class DefaultHubConfigService {
|
||||
existing.hubIdentifier === newConfig.hubIdentifier &&
|
||||
existing.libraryId === newConfig.libraryId
|
||||
);
|
||||
|
||||
// Check if name is being corrected (for logging)
|
||||
const nameChanged =
|
||||
existingConfig && existingConfig.name !== newConfig.name;
|
||||
if (nameChanged) {
|
||||
logger.info(
|
||||
`Correcting hub name from "${existingConfig.name}" to "${newConfig.name}"`,
|
||||
{
|
||||
label: 'Default Hub Config Service',
|
||||
hubIdentifier: newConfig.hubIdentifier,
|
||||
libraryId: newConfig.libraryId,
|
||||
oldName: existingConfig.name,
|
||||
newName: newConfig.name,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...newConfig,
|
||||
// Preserve existing ID or generate new one
|
||||
id: existingConfig?.id || IdGenerator.generateId(),
|
||||
// Preserve existing isActive status, or default to true for new configs
|
||||
isActive: existingConfig?.isActive ?? true,
|
||||
// Note: name comes from newConfig (discovery data), which fixes any broken names from the linking bug
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -144,8 +190,17 @@ export class DefaultHubConfigService {
|
||||
// For linked collections, preserve library-specific fields
|
||||
libraryId: configToUpdate.libraryId, // Don't change the library assignment
|
||||
libraryName: configToUpdate.libraryName, // Don't change the library name
|
||||
name: configToUpdate.name, // Don't change the hub display name (library-specific)
|
||||
hubIdentifier: configToUpdate.hubIdentifier, // Don't change the hub identifier
|
||||
mediaType: configToUpdate.mediaType, // Don't change the media type
|
||||
sortOrderHome: configToUpdate.sortOrderHome, // Library-specific ordering
|
||||
sortOrderLibrary: configToUpdate.sortOrderLibrary, // Library-specific ordering
|
||||
isLibraryPromoted: configToUpdate.isLibraryPromoted, // Library-specific promotion status
|
||||
everLibraryPromoted: configToUpdate.everLibraryPromoted, // Library-specific promotion history
|
||||
isPromotedToHub: configToUpdate.isPromotedToHub, // Library-specific promotability
|
||||
missing: configToUpdate.missing, // Library-specific existence status
|
||||
lastSyncedAt: configToUpdate.lastSyncedAt, // Library-specific sync timestamp
|
||||
needsSync: configToUpdate.needsSync, // Library-specific sync status
|
||||
// Note: isLinked, linkId, isUnlinked come from settings spread above
|
||||
};
|
||||
|
||||
@@ -177,6 +232,7 @@ export class DefaultHubConfigService {
|
||||
const existingHubConfigs = settings.plex.hubConfigs || [];
|
||||
|
||||
// Add isActive field and default time restrictions server-side
|
||||
// Also repairs any broken names from the linking bug (names are refreshed from Plex discovery data)
|
||||
const hubConfigsWithActiveStatus = newConfigs.map(
|
||||
(config: DiscoveredHubConfig) => {
|
||||
// Try to find existing hub by natural key to preserve ID
|
||||
@@ -186,6 +242,22 @@ export class DefaultHubConfigService {
|
||||
existing.libraryId === config.libraryId
|
||||
);
|
||||
|
||||
// Check if name is being corrected (for logging)
|
||||
const nameChanged =
|
||||
existingConfig && existingConfig.name !== config.name;
|
||||
if (nameChanged) {
|
||||
logger.info(
|
||||
`Correcting hub name from "${existingConfig.name}" to "${config.name}"`,
|
||||
{
|
||||
label: 'Default Hub Config Service',
|
||||
hubIdentifier: config.hubIdentifier,
|
||||
libraryId: config.libraryId,
|
||||
oldName: existingConfig.name,
|
||||
newName: config.name,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
// Preserve existing ID or generate new one
|
||||
@@ -194,6 +266,7 @@ export class DefaultHubConfigService {
|
||||
timeRestriction: config.timeRestriction || {
|
||||
alwaysActive: true, // Default to always active
|
||||
},
|
||||
// Note: name comes from config (discovery data), which fixes any broken names from the linking bug
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -264,11 +337,13 @@ export class DefaultHubConfigService {
|
||||
);
|
||||
|
||||
// Link all hubs in this group
|
||||
// BUT: respect isUnlinked flag - don't re-link deliberately unlinked hubs
|
||||
hubs.forEach((hub: PlexHubConfig) => {
|
||||
resultConfigs.push({
|
||||
...hub,
|
||||
isLinked: true,
|
||||
isLinked: hub.isUnlinked ? false : true, // Don't re-link if deliberately unlinked
|
||||
linkId,
|
||||
// Keep isUnlinked as-is - it remains true if user deliberately unlinked
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -186,6 +186,8 @@ export class DiscoveryService {
|
||||
await this.resetPreExistingPromotionStatus();
|
||||
|
||||
// STEP 3: Discover hubs and enhance pre-existing collections with hub data
|
||||
// Use an object to pass repairedHubNamesCount by reference so it can be modified
|
||||
const repairedHubNamesCounter = { count: 0 };
|
||||
await this.discoverHubsAndEnhance(
|
||||
plexClient,
|
||||
libraries,
|
||||
@@ -197,7 +199,9 @@ export class DiscoveryService {
|
||||
existingCollectionKeys,
|
||||
existingCollectionIds,
|
||||
allCollections,
|
||||
enhancedExistingConfigs
|
||||
enhancedExistingConfigs,
|
||||
existingHubConfigs,
|
||||
repairedHubNamesCounter
|
||||
);
|
||||
|
||||
// STEP 4: Promote collections that should be visible but aren't in hub management
|
||||
@@ -286,21 +290,31 @@ export class DiscoveryService {
|
||||
|
||||
// STEP 4: Update settings with discovered configs if requested
|
||||
if (updateSettings) {
|
||||
// Add discovered hub configs to settings
|
||||
// Add discovered hub configs to settings using DefaultHubConfigService
|
||||
// This ensures automatic linking is applied properly
|
||||
if (discoveredHubConfigs.length > 0) {
|
||||
const existingHubConfigs = settings.plex.hubConfigs || [];
|
||||
const newHubConfigs = [...existingHubConfigs];
|
||||
const { defaultHubConfigService } = await import(
|
||||
'@server/lib/collections/services/DefaultHubConfigService'
|
||||
);
|
||||
|
||||
for (const discoveredHub of discoveredHubConfigs) {
|
||||
// Add isActive: true to make it a complete PlexHubConfig
|
||||
newHubConfigs.push({ ...discoveredHub, isActive: true });
|
||||
}
|
||||
// Use appendConfigs which applies automatic linking logic
|
||||
// This also saves the repaired names from existing hubs
|
||||
defaultHubConfigService.appendConfigs(discoveredHubConfigs);
|
||||
|
||||
settings.plex.hubConfigs = newHubConfigs;
|
||||
logger.debug(
|
||||
`Added ${discoveredHubConfigs.length} new hub configs to settings`,
|
||||
logger.info(
|
||||
`Added ${discoveredHubConfigs.length} new hub configs to settings with automatic linking`,
|
||||
{
|
||||
label: 'Discovery Service',
|
||||
repairedNamesCount: repairedHubNamesCounter.count,
|
||||
}
|
||||
);
|
||||
} else if (repairedHubNamesCounter.count > 0) {
|
||||
// No new hubs, but we repaired existing hub names - save settings
|
||||
settings.save();
|
||||
logger.info(
|
||||
`Repaired ${repairedHubNamesCounter.count} existing hub names (no new hubs discovered)`,
|
||||
{
|
||||
label: 'Discovery Service - Name Repair',
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -715,7 +729,9 @@ export class DiscoveryService {
|
||||
existingCollectionKeys: Set<string>,
|
||||
existingCollectionIds: Set<string>,
|
||||
allCollections: PlexCollection[],
|
||||
enhancedExistingConfigs: PreExistingCollectionConfig[]
|
||||
enhancedExistingConfigs: PreExistingCollectionConfig[],
|
||||
existingHubConfigs: PlexHubConfig[],
|
||||
repairedHubNamesCounter: { count: number }
|
||||
): Promise<void> {
|
||||
// Counters for summary logging
|
||||
let skippedAgregarrCollections = 0;
|
||||
@@ -898,6 +914,28 @@ export class DiscoveryService {
|
||||
if (!existingHubKeys.has(hubKey)) {
|
||||
discoveredHubConfigs.push(hubConfig);
|
||||
processedHubs++;
|
||||
} else {
|
||||
// Hub already exists - check if name needs repair (from linking bug)
|
||||
const existingHub = existingHubConfigs.find(
|
||||
(h: PlexHubConfig) =>
|
||||
h.hubIdentifier === hubConfig.hubIdentifier &&
|
||||
h.libraryId === hubConfig.libraryId
|
||||
);
|
||||
if (existingHub && existingHub.name !== hubConfig.name) {
|
||||
logger.info(
|
||||
`Repairing hub name from "${existingHub.name}" to "${hubConfig.name}"`,
|
||||
{
|
||||
label: 'Discovery Service - Name Repair',
|
||||
hubIdentifier: hubConfig.hubIdentifier,
|
||||
libraryId: hubConfig.libraryId,
|
||||
oldName: existingHub.name,
|
||||
newName: hubConfig.name,
|
||||
}
|
||||
);
|
||||
// Update the existing hub's name directly
|
||||
existingHub.name = hubConfig.name;
|
||||
repairedHubNamesCounter.count++;
|
||||
}
|
||||
}
|
||||
} else if (parsedHub.ratingKey) {
|
||||
// This has a rating key - check if it's an Agregarr collection or pre-existing
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SMART_COLLECTION_SORT_OPTIONS } from '@app/types/collections';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { Field, Formik, type FormikErrors, type FormikTouched } from 'formik';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
@@ -120,6 +120,117 @@ const CollectionFormConfigForm = ({
|
||||
// State for preview modal
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
// Generate tooltip showing which other items will be affected - MUST be before early returns
|
||||
const linkingTooltip = useMemo(() => {
|
||||
if (!config) return undefined;
|
||||
|
||||
const isHub =
|
||||
config.collectionType === 'default_plex_hub' ||
|
||||
(config as CollectionFormConfig).configType === 'hub';
|
||||
const isPreExisting =
|
||||
config.collectionType === 'pre_existing' ||
|
||||
(config as CollectionFormConfig).configType === 'preExisting';
|
||||
const isCollection = !isHub && !isPreExisting;
|
||||
const isLinked = Boolean(config.isLinked && !config.isUnlinked);
|
||||
|
||||
if (isLinked) {
|
||||
// Unlink button - show what will be unlinked
|
||||
if (isHub && allHubConfigs) {
|
||||
const hubConfig = config as PlexHubConfig;
|
||||
const linkedHubs = allHubConfigs.filter(
|
||||
(h: PlexHubConfig) =>
|
||||
h.linkId === config.linkId && h.isLinked && h.id !== config.id
|
||||
);
|
||||
if (linkedHubs.length > 0) {
|
||||
const currentHubText = `${hubConfig.name} (${hubConfig.libraryName})`;
|
||||
const otherHubsText = linkedHubs
|
||||
.map((h) => `${h.name} (${h.libraryName})`)
|
||||
.join('\n');
|
||||
return `Will unlink ${
|
||||
linkedHubs.length + 1
|
||||
} hubs:\n${currentHubText}\n${otherHubsText}`;
|
||||
}
|
||||
} else if (isCollection && allCollectionConfigs) {
|
||||
const collectionConfig = config as CollectionFormConfig;
|
||||
const linkedCollections = allCollectionConfigs.filter(
|
||||
(c: CollectionFormConfig) =>
|
||||
c.type === collectionConfig.type &&
|
||||
c.subtype === collectionConfig.subtype &&
|
||||
c.linkId === collectionConfig.linkId &&
|
||||
c.isLinked &&
|
||||
c.id !== collectionConfig.id
|
||||
);
|
||||
if (linkedCollections.length > 0) {
|
||||
const currentLibName =
|
||||
libraries?.find((lib) => lib.key === collectionConfig.libraryId)
|
||||
?.name || 'Unknown';
|
||||
const currentText = `${config.name} (${currentLibName})`;
|
||||
const otherTexts = linkedCollections
|
||||
.map((c) => {
|
||||
const libName =
|
||||
libraries?.find((lib) => lib.key === c.libraryId)?.name ||
|
||||
'Unknown';
|
||||
return `${c.name} (${libName})`;
|
||||
})
|
||||
.join('\n');
|
||||
return `Will unlink ${
|
||||
linkedCollections.length + 1
|
||||
} collections:\n${currentText}\n${otherTexts}`;
|
||||
}
|
||||
}
|
||||
} else if (config.linkId) {
|
||||
// Check if can link
|
||||
if (isHub && allHubConfigs) {
|
||||
const hubConfig = config as PlexHubConfig;
|
||||
const eligibleHubs = allHubConfigs.filter(
|
||||
(h: PlexHubConfig) =>
|
||||
h.linkId !== undefined &&
|
||||
h.linkId === config.linkId &&
|
||||
h.id !== config.id &&
|
||||
!h.isLinked
|
||||
);
|
||||
if (eligibleHubs.length > 0) {
|
||||
const currentHubText = `${hubConfig.name} (${hubConfig.libraryName})`;
|
||||
const otherHubsText = eligibleHubs
|
||||
.map((h) => `${h.name} (${h.libraryName})`)
|
||||
.join('\n');
|
||||
return `Will link ${
|
||||
eligibleHubs.length + 1
|
||||
} hubs:\n${currentHubText}\n${otherHubsText}`;
|
||||
}
|
||||
} else if (isCollection && allCollectionConfigs) {
|
||||
const collectionConfig = config as CollectionFormConfig;
|
||||
const eligibleCollections = allCollectionConfigs.filter(
|
||||
(c: CollectionFormConfig) =>
|
||||
c.type === collectionConfig.type &&
|
||||
c.subtype === collectionConfig.subtype &&
|
||||
c.linkId !== undefined &&
|
||||
c.linkId === collectionConfig.linkId &&
|
||||
!c.isLinked &&
|
||||
c.id !== collectionConfig.id
|
||||
);
|
||||
if (eligibleCollections.length > 0) {
|
||||
const currentLibName =
|
||||
libraries?.find((lib) => lib.key === collectionConfig.libraryId)
|
||||
?.name || 'Unknown';
|
||||
const currentText = `${config.name} (${currentLibName})`;
|
||||
const otherTexts = eligibleCollections
|
||||
.map((c) => {
|
||||
const libName =
|
||||
libraries?.find((lib) => lib.key === c.libraryId)?.name ||
|
||||
'Unknown';
|
||||
return `${c.name} (${libName})`;
|
||||
})
|
||||
.join('\n');
|
||||
return `Will link ${
|
||||
eligibleCollections.length + 1
|
||||
} collections:\n${currentText}\n${otherTexts}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}, [config, allHubConfigs, allCollectionConfigs, libraries]);
|
||||
|
||||
// Validation schema for collections, hubs, and pre-existing configs
|
||||
const CollectionFormConfigSchema = Yup.object().shape({
|
||||
// Only validate type/subtype for full collections, not hubs/pre-existing
|
||||
@@ -478,7 +589,8 @@ const CollectionFormConfigForm = ({
|
||||
const isCollection = !isHub && !isPreExisting; // Regular Agregarr collections
|
||||
|
||||
// Use unified linking approach - check if actively linked
|
||||
const isLinked = Boolean(config.isLinked);
|
||||
// If isUnlinked is true, treat as NOT linked (available for re-linking)
|
||||
const isLinked = Boolean(config.isLinked && !config.isUnlinked);
|
||||
|
||||
// Determine if this config can be linked (for showing link button)
|
||||
// Only show link button for existing configs that are unlinked but could be linked
|
||||
@@ -489,9 +601,14 @@ const CollectionFormConfigForm = ({
|
||||
// For hubs: check if there are other unlinked hubs with same linkId
|
||||
// Include hubs with isUnlinked flag - those can be relinked!
|
||||
if (!allHubConfigs) return false;
|
||||
// Must have valid linkId to be linkable (prevent undefined === undefined)
|
||||
if (config.linkId === undefined) return false;
|
||||
const eligibleHubs = allHubConfigs.filter(
|
||||
(h: PlexHubConfig) =>
|
||||
h.linkId === config.linkId && h.id !== config.id && !h.isLinked
|
||||
h.linkId !== undefined && // Must have valid linkId
|
||||
h.linkId === config.linkId &&
|
||||
h.id !== config.id &&
|
||||
!h.isLinked
|
||||
// Note: We don't exclude isUnlinked hubs - they can be relinked
|
||||
);
|
||||
return eligibleHubs.length > 0;
|
||||
@@ -499,10 +616,13 @@ const CollectionFormConfigForm = ({
|
||||
// For collections: check if there are other unlinked collections with same type/subtype/linkId
|
||||
if (!allCollectionConfigs) return false;
|
||||
const collectionConfig = config as CollectionFormConfig;
|
||||
// Must have valid linkId to be linkable (prevent undefined === undefined)
|
||||
if (collectionConfig.linkId === undefined) return false;
|
||||
const eligibleCollections = allCollectionConfigs.filter(
|
||||
(c: CollectionFormConfig) =>
|
||||
c.type === collectionConfig.type &&
|
||||
c.subtype === collectionConfig.subtype &&
|
||||
c.linkId !== undefined && // Must have valid linkId
|
||||
c.linkId === collectionConfig.linkId &&
|
||||
!c.isLinked &&
|
||||
c.id !== collectionConfig.id
|
||||
@@ -1244,6 +1364,7 @@ const CollectionFormConfigForm = ({
|
||||
: 'Link'
|
||||
: undefined
|
||||
}
|
||||
secondaryTooltip={linkingTooltip}
|
||||
secondaryButtonType={isLinked ? 'warning' : 'primary'}
|
||||
// Add preview button for collections (not hubs or pre-existing)
|
||||
// Disable for overseerr individual user requests (type=overseerr, subtype=users)
|
||||
|
||||
@@ -10,6 +10,10 @@ import PageTitle from '@app/components/Common/PageTitle';
|
||||
import { useCollectionEdit } from '@app/hooks/collections/useCollectionEdit';
|
||||
import type { CollectionFormConfig, Library } from '@app/types/collections';
|
||||
import { formatSyncScheduleBadge } from '@app/utils/collections/collectionUtils';
|
||||
import {
|
||||
linkCollectionConfig,
|
||||
unlinkCollectionConfig,
|
||||
} from '@app/utils/collections/linkingHandlers';
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
CheckIcon,
|
||||
@@ -27,8 +31,9 @@ import type {
|
||||
} from '@server/lib/settings';
|
||||
import axios from 'axios';
|
||||
import type React from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -65,6 +70,7 @@ interface DisplayCollection {
|
||||
|
||||
const AllCollectionsView: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { addToast } = useToasts();
|
||||
|
||||
// Use the shared collection edit hook for collections only
|
||||
const {
|
||||
@@ -90,9 +96,11 @@ const AllCollectionsView: React.FC = () => {
|
||||
const [filterLibrary, setFilterLibrary] = useState<string>('all');
|
||||
|
||||
// Fetch data from separate APIs for consistency with CollectionSettings
|
||||
const { data: collectionData, error: collectionError } = useSWR(
|
||||
'/api/v1/collections'
|
||||
);
|
||||
const {
|
||||
data: collectionData,
|
||||
error: collectionError,
|
||||
mutate: revalidateCollections,
|
||||
} = useSWR('/api/v1/collections');
|
||||
const { data: libraries = [], error: librariesError } = useSWR(
|
||||
'/api/v1/settings/plex/libraries'
|
||||
);
|
||||
@@ -107,6 +115,37 @@ const AllCollectionsView: React.FC = () => {
|
||||
mutate: revalidatePreExisting,
|
||||
} = useSWR('/api/v1/preexisting');
|
||||
|
||||
// Local state for linking operations
|
||||
const [localCollectionConfigs, setLocalCollectionConfigs] = useState<
|
||||
CollectionFormConfig[]
|
||||
>([]);
|
||||
const [localHubConfigs, setLocalHubConfigs] = useState<PlexHubConfig[]>([]);
|
||||
|
||||
// Update local state when data changes
|
||||
useEffect(() => {
|
||||
if (collectionData?.collectionConfigs) {
|
||||
setLocalCollectionConfigs(collectionData.collectionConfigs);
|
||||
}
|
||||
if (hubConfigs) {
|
||||
setLocalHubConfigs(hubConfigs);
|
||||
}
|
||||
}, [collectionData, hubConfigs]);
|
||||
|
||||
// Revalidate all data sources
|
||||
const revalidateAll = () => {
|
||||
revalidateCollections();
|
||||
revalidateDefaultHubs();
|
||||
revalidatePreExisting();
|
||||
};
|
||||
|
||||
// Wrapper for saveCollectionConfig to match linking handler signature
|
||||
const saveCollectionConfigs = async (configs: CollectionFormConfig[]) => {
|
||||
// Save each config individually
|
||||
for (const config of configs) {
|
||||
await saveCollectionConfig(config);
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
!collectionData || !libraries || !hubConfigs || !preExistingConfigs;
|
||||
const hasError =
|
||||
@@ -1218,6 +1257,32 @@ const AllCollectionsView: React.FC = () => {
|
||||
onSave={saveCollectionConfig}
|
||||
onCancel={closeCollectionModal}
|
||||
libraries={libraries}
|
||||
onUnlink={(config) =>
|
||||
unlinkCollectionConfig(config, {
|
||||
localCollectionConfigs:
|
||||
collectionData?.collectionConfigs || localCollectionConfigs,
|
||||
localHubConfigs: hubConfigs || localHubConfigs,
|
||||
setLocalCollectionConfigs,
|
||||
setLocalHubConfigs,
|
||||
revalidateAll,
|
||||
addToast,
|
||||
saveCollectionConfigs,
|
||||
})
|
||||
}
|
||||
onLink={(config) =>
|
||||
linkCollectionConfig(config, {
|
||||
localCollectionConfigs:
|
||||
collectionData?.collectionConfigs || localCollectionConfigs,
|
||||
localHubConfigs: hubConfigs || localHubConfigs,
|
||||
setLocalCollectionConfigs,
|
||||
setLocalHubConfigs,
|
||||
revalidateAll,
|
||||
addToast,
|
||||
saveCollectionConfigs,
|
||||
})
|
||||
}
|
||||
allCollectionConfigs={collectionData?.collectionConfigs || []}
|
||||
allHubConfigs={hubConfigs || []}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1228,6 +1293,32 @@ const AllCollectionsView: React.FC = () => {
|
||||
onSave={saveHubConfig}
|
||||
onCancel={closeHubModal}
|
||||
libraries={libraries}
|
||||
onUnlink={(config) =>
|
||||
unlinkCollectionConfig(config, {
|
||||
localCollectionConfigs:
|
||||
collectionData?.collectionConfigs || localCollectionConfigs,
|
||||
localHubConfigs: hubConfigs || localHubConfigs,
|
||||
setLocalCollectionConfigs,
|
||||
setLocalHubConfigs,
|
||||
revalidateAll,
|
||||
addToast,
|
||||
saveCollectionConfigs,
|
||||
})
|
||||
}
|
||||
onLink={(config) =>
|
||||
linkCollectionConfig(config, {
|
||||
localCollectionConfigs:
|
||||
collectionData?.collectionConfigs || localCollectionConfigs,
|
||||
localHubConfigs: hubConfigs || localHubConfigs,
|
||||
setLocalCollectionConfigs,
|
||||
setLocalHubConfigs,
|
||||
revalidateAll,
|
||||
addToast,
|
||||
saveCollectionConfigs,
|
||||
})
|
||||
}
|
||||
allCollectionConfigs={collectionData?.collectionConfigs || []}
|
||||
allHubConfigs={hubConfigs || []}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1238,6 +1329,32 @@ const AllCollectionsView: React.FC = () => {
|
||||
onSave={savePreExistingConfig}
|
||||
onCancel={closePreExistingModal}
|
||||
libraries={libraries}
|
||||
onUnlink={(config) =>
|
||||
unlinkCollectionConfig(config, {
|
||||
localCollectionConfigs:
|
||||
collectionData?.collectionConfigs || localCollectionConfigs,
|
||||
localHubConfigs: hubConfigs || localHubConfigs,
|
||||
setLocalCollectionConfigs,
|
||||
setLocalHubConfigs,
|
||||
revalidateAll,
|
||||
addToast,
|
||||
saveCollectionConfigs,
|
||||
})
|
||||
}
|
||||
onLink={(config) =>
|
||||
linkCollectionConfig(config, {
|
||||
localCollectionConfigs:
|
||||
collectionData?.collectionConfigs || localCollectionConfigs,
|
||||
localHubConfigs: hubConfigs || localHubConfigs,
|
||||
setLocalCollectionConfigs,
|
||||
setLocalHubConfigs,
|
||||
revalidateAll,
|
||||
addToast,
|
||||
saveCollectionConfigs,
|
||||
})
|
||||
}
|
||||
allCollectionConfigs={collectionData?.collectionConfigs || []}
|
||||
allHubConfigs={hubConfigs || []}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -20,6 +20,7 @@ interface ModalProps {
|
||||
cancelText?: string;
|
||||
okText?: string;
|
||||
secondaryText?: string;
|
||||
secondaryTooltip?: string;
|
||||
tertiaryText?: string;
|
||||
okDisabled?: boolean;
|
||||
cancelButtonType?: ButtonType;
|
||||
@@ -56,6 +57,7 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
||||
secondaryDisabled = false,
|
||||
onSecondary,
|
||||
secondaryText,
|
||||
secondaryTooltip,
|
||||
tertiaryButtonType = 'default',
|
||||
tertiaryDisabled = false,
|
||||
tertiaryText,
|
||||
@@ -236,6 +238,7 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>(
|
||||
className="ml-3"
|
||||
disabled={secondaryDisabled}
|
||||
data-testid="modal-secondary-button"
|
||||
title={secondaryTooltip}
|
||||
>
|
||||
{secondaryText}
|
||||
</Button>
|
||||
|
||||
@@ -41,8 +41,21 @@ export const linkCollectionConfig = async (
|
||||
);
|
||||
if (!currentHub) return;
|
||||
|
||||
// Hubs must have a valid linkId to be linkable (prevent undefined === undefined matching ALL hubs)
|
||||
if (currentHub.linkId === undefined) {
|
||||
addToast(
|
||||
'This hub does not have a link group ID. Please run hub discovery to enable linking.',
|
||||
{
|
||||
autoDismiss: true,
|
||||
appearance: 'warning',
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const eligibleHubs = localHubConfigs.filter(
|
||||
(h: PlexHubConfig) =>
|
||||
h.linkId !== undefined && // Must have a valid linkId (prevent undefined === undefined)
|
||||
h.linkId === currentHub.linkId && // Same linkId group (established during discovery)
|
||||
h.id !== config.id &&
|
||||
!h.isLinked // Only link hubs that aren't already linked
|
||||
@@ -250,6 +263,7 @@ export const unlinkCollectionConfig = async (
|
||||
const currentHub = localHubConfigs.find(
|
||||
(h: PlexHubConfig) => h.id === config.id
|
||||
);
|
||||
|
||||
if (!currentHub || !currentHub.isLinked || !currentHub.linkId) {
|
||||
addToast('This hub is not linked to any other hubs.', {
|
||||
autoDismiss: true,
|
||||
|
||||
Reference in New Issue
Block a user