@@ -1757,17 +1757,18 @@ export abstract class TaskModal extends Modal {
17571757 }
17581758
17591759 protected addProject ( file : TAbstractFile ) : void {
1760- // Avoid duplicates
1761- if ( this . selectedProjectItems . some ( ( existing ) => existing . file ?. path === file . path ) ) {
1762- return ;
1763- }
1764-
17651760 if ( file instanceof TFile ) {
1766- this . selectedProjectItems . push ( {
1761+ const projectItem = {
17671762 file,
17681763 name : file . basename ,
17691764 link : this . buildProjectReference ( file , this . getCurrentTaskPath ( ) || "" ) ,
1770- } ) ;
1765+ } ;
1766+
1767+ if ( this . hasProjectItem ( projectItem ) ) {
1768+ return ;
1769+ }
1770+
1771+ this . selectedProjectItems . push ( projectItem ) ;
17711772 }
17721773 this . updateProjectsFromFiles ( ) ;
17731774 this . renderProjectsList ( ) ;
@@ -1798,91 +1799,133 @@ export abstract class TaskModal extends Modal {
17981799 }
17991800
18001801 protected initializeProjectsFromStrings ( projects : string [ ] ) : void {
1801- // Convert project string to ProjectItem objects
1802- // This handles both old plain string projects and new [[link]] format
18031802 this . selectedProjectItems = [ ] ;
1803+ this . addProjectsFromStrings ( projects ) ;
1804+ // Don't render immediately - let the caller decide when to render
1805+ }
1806+
1807+ protected addProjectsFromStrings ( projects : string [ ] ) : void {
1808+ // Convert project strings to ProjectItem objects.
1809+ // This handles both old plain string projects and new [[link]] format.
18041810
18051811 // Use the task's path as the source for resolving relative links
18061812 const sourcePath = this . getCurrentTaskPath ( ) || "" ;
18071813
18081814 for ( const projectString of projects ) {
1809- // Skip null, undefined, or empty strings
1810- if (
1811- ! projectString ||
1812- typeof projectString !== "string" ||
1813- projectString . trim ( ) === ""
1814- ) {
1815- continue ;
1816- }
1815+ const projectItem = this . createProjectItemFromString ( projectString , sourcePath ) ;
1816+ if ( ! projectItem || this . hasProjectItem ( projectItem ) ) continue ;
1817+ this . selectedProjectItems . push ( projectItem ) ;
1818+ }
1819+ this . updateProjectsFromFiles ( ) ;
1820+ // Don't render immediately - let the caller decide when to render
1821+ }
18171822
1818- // Check if it's a wiki link format
1819- const linkMatch = projectString . match ( / ^ \[ \[ ( [ ^ \] ] + ) \] \] $ / ) ;
1820- if ( linkMatch ) {
1821- const linkPath = linkMatch [ 1 ] ;
1823+ private createProjectItemFromString ( projectString : string , sourcePath : string ) : ProjectItem | null {
1824+ // Skip null, undefined, or empty strings
1825+ if (
1826+ ! projectString ||
1827+ typeof projectString !== "string" ||
1828+ projectString . trim ( ) === ""
1829+ ) {
1830+ return null ;
1831+ }
1832+
1833+ // Check if it's a wiki link format
1834+ const linkMatch = projectString . match ( / ^ \[ \[ ( [ ^ \] ] + ) \] \] $ / ) ;
1835+ if ( linkMatch ) {
1836+ const linkPath = linkMatch [ 1 ] ;
1837+ const file = this . resolveLink ( linkPath , sourcePath ) ;
1838+ if ( file ) {
1839+ // Resolved link
1840+ return {
1841+ file,
1842+ name : file . basename ,
1843+ link : projectString ,
1844+ } ;
1845+ } else {
1846+ // Unresolved link - still add it!
1847+ const displayName = linkPath . split ( "|" ) [ 0 ] ; // Strip alias if present
1848+ return {
1849+ name : displayName ,
1850+ link : projectString ,
1851+ unresolved : true ,
1852+ } ;
1853+ }
1854+ } else {
1855+ // Check if it's a markdown link format [text](path)
1856+ const markdownMatch = projectString . match ( / ^ \[ ( [ ^ \] ] * ) \] \( ( [ ^ ) ] + ) \) $ / ) ;
1857+ if ( markdownMatch ) {
1858+ const linkPath = parseLinkToPath ( projectString ) ;
18221859 const file = this . resolveLink ( linkPath , sourcePath ) ;
18231860 if ( file ) {
1824- // Resolved link
1825- this . selectedProjectItems . push ( {
1861+ // Resolved markdown link
1862+ return {
18261863 file,
18271864 name : file . basename ,
18281865 link : projectString ,
1829- } ) ;
1866+ } ;
18301867 } else {
1831- // Unresolved link - still add it!
1832- const displayName = linkPath . split ( "|" ) [ 0 ] ; // Strip alias if present
1833- this . selectedProjectItems . push ( {
1868+ // Unresolved markdown link
1869+ const displayName = markdownMatch [ 1 ] || linkPath ;
1870+ return {
18341871 name : displayName ,
18351872 link : projectString ,
18361873 unresolved : true ,
1837- } ) ;
1874+ } ;
18381875 }
18391876 } else {
1840- // Check if it's a markdown link format [text](path)
1841- const markdownMatch = projectString . match ( / ^ \[ ( [ ^ \] ] * ) \] \( ( [ ^ ) ] + ) \) $ / ) ;
1842- if ( markdownMatch ) {
1843- const linkPath = parseLinkToPath ( projectString ) ;
1844- const file = this . resolveLink ( linkPath , sourcePath ) ;
1845- if ( file ) {
1846- // Resolved markdown link
1847- this . selectedProjectItems . push ( {
1848- file,
1849- name : file . basename ,
1850- link : projectString ,
1851- } ) ;
1852- } else {
1853- // Unresolved markdown link
1854- const displayName = markdownMatch [ 1 ] || linkPath ;
1855- this . selectedProjectItems . push ( {
1856- name : displayName ,
1857- link : projectString ,
1858- unresolved : true ,
1859- } ) ;
1860- }
1877+ // For backwards compatibility, try to find a file with this name
1878+ const files = this . getMarkdownFiles ( ) ;
1879+ const matchingFile = files . find (
1880+ ( f ) => f . basename === projectString || f . name === projectString + ".md"
1881+ ) ;
1882+ if ( matchingFile ) {
1883+ return {
1884+ file : matchingFile ,
1885+ name : matchingFile . basename ,
1886+ link : `[[${ matchingFile . basename } ]]` ,
1887+ } ;
18611888 } else {
1862- // For backwards compatibility, try to find a file with this name
1863- const files = this . getMarkdownFiles ( ) ;
1864- const matchingFile = files . find (
1865- ( f ) => f . basename === projectString || f . name === projectString + ".md"
1866- ) ;
1867- if ( matchingFile ) {
1868- this . selectedProjectItems . push ( {
1869- file : matchingFile ,
1870- name : matchingFile . basename ,
1871- link : `[[${ matchingFile . basename } ]]` ,
1872- } ) ;
1873- } else {
1874- // Plain text - preserve as-is
1875- this . selectedProjectItems . push ( {
1876- name : projectString ,
1877- link : projectString ,
1878- unresolved : true ,
1879- } ) ;
1880- }
1889+ // Plain text - preserve as-is
1890+ return {
1891+ name : projectString ,
1892+ link : projectString ,
1893+ unresolved : true ,
1894+ } ;
18811895 }
18821896 }
18831897 }
1884- this . updateProjectsFromFiles ( ) ;
1885- // Don't render immediately - let the caller decide when to render
1898+ }
1899+
1900+ private hasProjectItem ( candidate : ProjectItem ) : boolean {
1901+ const candidateKeys = this . getProjectDedupKeys ( candidate ) ;
1902+ return this . selectedProjectItems . some ( ( existing ) => {
1903+ const existingKeys = this . getProjectDedupKeys ( existing ) ;
1904+ return candidateKeys . some ( ( key ) => existingKeys . includes ( key ) ) ;
1905+ } ) ;
1906+ }
1907+
1908+ private getProjectDedupKeys ( item : ProjectItem ) : string [ ] {
1909+ const keys = new Set < string > ( ) ;
1910+
1911+ if ( item . file ?. path ) {
1912+ keys . add ( `path:${ this . normalizeProjectPath ( item . file . path ) } ` ) ;
1913+ }
1914+
1915+ const parsedPath = parseLinkToPath ( item . link ) ;
1916+ if ( parsedPath ) {
1917+ keys . add ( `path:${ this . normalizeProjectPath ( parsedPath ) } ` ) ;
1918+ }
1919+
1920+ if ( item . link ) {
1921+ keys . add ( `link:${ item . link . trim ( ) . toLowerCase ( ) } ` ) ;
1922+ }
1923+
1924+ return Array . from ( keys ) ;
1925+ }
1926+
1927+ private normalizeProjectPath ( path : string ) : string {
1928+ return path . trim ( ) . replace ( / \. m d $ / i, "" ) . toLowerCase ( ) ;
18861929 }
18871930
18881931 protected renderProjectsList ( ) : void {
0 commit comments