@devstroupe/devkit-cli 1.1.8 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +137 -124
- package/dist/templates/nest/migration.template.js +21 -15
- package/dist/templates/relationship-templates.test.js +3 -2
- package/dist/templates/ui/layout/header.template.js +104 -15
- package/dist/templates/ui/layout/main-layout.template.js +7 -0
- package/dist/templates/ui/shared/theme-service.template.js +30 -15
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -955,13 +955,13 @@ program
|
|
|
955
955
|
process.exit(0);
|
|
956
956
|
}
|
|
957
957
|
});
|
|
958
|
-
// Comando generate crud
|
|
958
|
+
// Comando generate crud ou project
|
|
959
959
|
program
|
|
960
|
-
.command('generate <type>
|
|
961
|
-
.description('Gera artefatos do DevKit. Tipos suportados: crud')
|
|
960
|
+
.command('generate <type> [entityName]')
|
|
961
|
+
.description('Gera artefatos do DevKit. Tipos suportados: crud, project')
|
|
962
962
|
.action((type, entityName) => {
|
|
963
|
-
if (type !== 'crud') {
|
|
964
|
-
console.error(`Tipo de geração "${type}" não é suportado. Use "crud".`);
|
|
963
|
+
if (type !== 'crud' && type !== 'project') {
|
|
964
|
+
console.error(`Tipo de geração "${type}" não é suportado. Use "crud" ou "project".`);
|
|
965
965
|
process.exit(1);
|
|
966
966
|
}
|
|
967
967
|
const configPath = path.join(process.cwd(), 'devstroupe.config.ts');
|
|
@@ -978,6 +978,14 @@ program
|
|
|
978
978
|
console.error('Erro ao carregar devstroupe.config.ts:', error);
|
|
979
979
|
process.exit(1);
|
|
980
980
|
}
|
|
981
|
+
if (type === 'project') {
|
|
982
|
+
generateProject(process.cwd(), config);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (!entityName) {
|
|
986
|
+
console.error('Nome da entidade não fornecido. Use "devstroupe generate crud all" ou "devstroupe generate crud <nome_da_entidade>".');
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
981
989
|
try {
|
|
982
990
|
assertValidRelationshipConfig(config);
|
|
983
991
|
}
|
|
@@ -1862,19 +1870,22 @@ volumes:
|
|
|
1862
1870
|
fs.writeFileSync(path.join(projectRoot, 'docker-compose.dev.yml'), dockerComposeDevContent, 'utf-8');
|
|
1863
1871
|
console.log(`[Docker] docker-compose.dev.yml gerado com sucesso.`);
|
|
1864
1872
|
}
|
|
1865
|
-
//
|
|
1866
|
-
|
|
1867
|
-
.
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
.
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1873
|
+
// Função que gera a estrutura de pastas e arquivos base a partir de um devstroupe.config.ts existente
|
|
1874
|
+
function generateProject(projectRoot, config) {
|
|
1875
|
+
const projectName = config.projectName || 'DevsTroupe Project';
|
|
1876
|
+
const isMicroservices = config.backendType === 'microservices';
|
|
1877
|
+
const frontendPathRel = config.frontendPath || './frontend';
|
|
1878
|
+
const backendPathRel = config.backendPath || './backend';
|
|
1879
|
+
const frontendDest = path.resolve(projectRoot, frontendPathRel);
|
|
1880
|
+
let backendDest;
|
|
1881
|
+
if (isMicroservices) {
|
|
1882
|
+
backendDest = path.resolve(projectRoot, backendPathRel, 'api-gateway');
|
|
1883
|
+
fs.ensureDirSync(path.resolve(projectRoot, backendPathRel, 'services'));
|
|
1875
1884
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1885
|
+
else {
|
|
1886
|
+
backendDest = path.resolve(projectRoot, backendPathRel);
|
|
1887
|
+
}
|
|
1888
|
+
console.log(`Configurando estrutura física do projeto "${projectName}" (Modo: ${isMicroservices ? 'Microsserviços' : 'Monólito'})...`);
|
|
1878
1889
|
// Resolver caminhos de boilerplates
|
|
1879
1890
|
let boilerplatesDir = path.resolve(__dirname, 'boilerplates');
|
|
1880
1891
|
if (!fs.existsSync(boilerplatesDir)) {
|
|
@@ -1890,25 +1901,14 @@ program
|
|
|
1890
1901
|
process.exit(1);
|
|
1891
1902
|
}
|
|
1892
1903
|
try {
|
|
1893
|
-
// 1. Criar diretórios de destino
|
|
1894
|
-
fs.
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
else {
|
|
1902
|
-
backendDest = path.join(projectRoot, 'backend');
|
|
1903
|
-
}
|
|
1904
|
-
// 2. Copiar boilerplates
|
|
1905
|
-
console.log(`Copiando boilerplate do backend (${isMicroservices ? 'API Gateway' : 'NestJS Monólito'})...`);
|
|
1906
|
-
fs.copySync(nestTemplateDir, backendDest, { filter: createBoilerplateCopyFilter(nestTemplateDir) });
|
|
1907
|
-
console.log('Copiando boilerplate do frontend (Angular)...');
|
|
1908
|
-
fs.copySync(angularTemplateDir, frontendDest, { filter: createBoilerplateCopyFilter(angularTemplateDir) });
|
|
1909
|
-
// 2.1 Criar arquivos .env e .env.example no backend
|
|
1910
|
-
console.log('Gerando arquivos .env e .env.example no backend...');
|
|
1911
|
-
const envContent = `PORT=13000
|
|
1904
|
+
// 1. Criar diretórios de destino e copiar boilerplates
|
|
1905
|
+
if (!fs.existsSync(backendDest) || fs.readdirSync(backendDest).length === 0) {
|
|
1906
|
+
console.log(`Copiando boilerplate do backend para: ${backendDest}...`);
|
|
1907
|
+
fs.ensureDirSync(backendDest);
|
|
1908
|
+
fs.copySync(nestTemplateDir, backendDest, { filter: createBoilerplateCopyFilter(nestTemplateDir) });
|
|
1909
|
+
// Gerar arquivos .env e .env.example
|
|
1910
|
+
console.log('Gerando arquivos .env e .env.example no backend...');
|
|
1911
|
+
const envContent = `PORT=13000
|
|
1912
1912
|
DB_HOST=localhost
|
|
1913
1913
|
DB_PORT=3306
|
|
1914
1914
|
DB_USER=root
|
|
@@ -1931,90 +1931,20 @@ R2_ACCOUNT_ID=
|
|
|
1931
1931
|
R2_ACCESS_KEY=
|
|
1932
1932
|
R2_SECRET_KEY=
|
|
1933
1933
|
`;
|
|
1934
|
-
|
|
1935
|
-
|
|
1934
|
+
fs.writeFileSync(path.join(backendDest, '.env'), envContent, 'utf-8');
|
|
1935
|
+
fs.writeFileSync(path.join(backendDest, '.env.example'), envContent, 'utf-8');
|
|
1936
|
+
}
|
|
1937
|
+
if (!fs.existsSync(frontendDest) || fs.readdirSync(frontendDest).length === 0) {
|
|
1938
|
+
console.log(`Copiando boilerplate do frontend para: ${frontendDest}...`);
|
|
1939
|
+
fs.ensureDirSync(frontendDest);
|
|
1940
|
+
fs.copySync(angularTemplateDir, frontendDest, { filter: createBoilerplateCopyFilter(angularTemplateDir) });
|
|
1941
|
+
}
|
|
1936
1942
|
// Patch local dependencies if in monorepo development
|
|
1937
1943
|
patchLocalDependencies(backendDest, frontendDest, boilerplatesDir);
|
|
1938
|
-
//
|
|
1939
|
-
console.log('Gerando arquivo devstroupe.config.ts...');
|
|
1940
|
-
const configPath = path.join(projectRoot, 'devstroupe.config.ts');
|
|
1941
|
-
const designSystemConfig = {
|
|
1942
|
-
provider: 'spartan',
|
|
1943
|
-
spartanStyle: 'vega',
|
|
1944
|
-
spartanTheme: 'neutral',
|
|
1945
|
-
installAllPrimitives: true,
|
|
1946
|
-
includePlayground: true,
|
|
1947
|
-
};
|
|
1948
|
-
const configContent = `export default {
|
|
1949
|
-
projectName: '${projectName}',
|
|
1950
|
-
backendType: '${isMicroservices ? 'microservices' : 'monolith'}',
|
|
1951
|
-
backendPath: './backend',
|
|
1952
|
-
frontendPath: './frontend',
|
|
1953
|
-
designSystem: {
|
|
1954
|
-
provider: '${designSystemConfig.provider}',
|
|
1955
|
-
spartanStyle: '${designSystemConfig.spartanStyle}',
|
|
1956
|
-
spartanTheme: '${designSystemConfig.spartanTheme}',
|
|
1957
|
-
installAllPrimitives: ${designSystemConfig.installAllPrimitives},
|
|
1958
|
-
includePlayground: ${designSystemConfig.includePlayground}
|
|
1959
|
-
},
|
|
1960
|
-
entities: [
|
|
1961
|
-
{
|
|
1962
|
-
name: 'company',
|
|
1963
|
-
label: 'Empresa',
|
|
1964
|
-
tableName: 'companies',
|
|
1965
|
-
properties: [
|
|
1966
|
-
{ name: 'name', type: 'string', required: true, label: 'Nome' },
|
|
1967
|
-
{
|
|
1968
|
-
name: 'cabins', type: 'relationship', required: false, label: 'Cabines',
|
|
1969
|
-
relation: {
|
|
1970
|
-
target: 'cabin', kind: 'one-to-many', foreignKey: 'company_id',
|
|
1971
|
-
inverseSide: 'company', control: 'table', embedded: true,
|
|
1972
|
-
columns: ['name', 'code', 'is_active'], lookup: { pageSize: 10 }
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
],
|
|
1976
|
-
crud: { generateFrontend: true, generateBackend: true }
|
|
1977
|
-
},
|
|
1978
|
-
{
|
|
1979
|
-
name: 'cabin',
|
|
1980
|
-
label: 'Cabine',
|
|
1981
|
-
tableName: 'cabins',
|
|
1982
|
-
properties: [
|
|
1983
|
-
{ name: 'name', type: 'string', required: true, label: 'Nome' },
|
|
1984
|
-
{ name: 'code', type: 'string', required: false, label: 'Código' },
|
|
1985
|
-
{
|
|
1986
|
-
name: 'company_id', type: 'relationship', required: true, label: 'Empresa',
|
|
1987
|
-
relation: {
|
|
1988
|
-
target: 'company', kind: 'many-to-one', relationName: 'company',
|
|
1989
|
-
inverseSide: 'cabins', control: 'select', displayField: 'name', onDelete: 'CASCADE',
|
|
1990
|
-
lookup: { mode: 'remote', pageSize: 20, minSearchLength: 0 }
|
|
1991
|
-
}
|
|
1992
|
-
},
|
|
1993
|
-
{ name: 'is_active', type: 'boolean', required: true, label: 'Ativo', default: true }
|
|
1994
|
-
],
|
|
1995
|
-
crud: {
|
|
1996
|
-
generateFrontend: true,
|
|
1997
|
-
generateBackend: true
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
]
|
|
2001
|
-
};
|
|
2002
|
-
`;
|
|
2003
|
-
fs.writeFileSync(configPath, configContent, 'utf-8');
|
|
2004
|
-
// 3.1 Criar docker-compose.yml e init.sql
|
|
1944
|
+
// 2. Gerar docker-compose.yml e init.sql
|
|
2005
1945
|
console.log('Gerando docker-compose.yml e base de dados...');
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
backendType: isMicroservices ? 'microservices' : 'monolith',
|
|
2009
|
-
entities: [
|
|
2010
|
-
{
|
|
2011
|
-
name: 'cabin',
|
|
2012
|
-
tableName: 'cabins',
|
|
2013
|
-
}
|
|
2014
|
-
]
|
|
2015
|
-
};
|
|
2016
|
-
generateDockerComposeAndDbInit(projectRoot, tempConfig);
|
|
2017
|
-
// 3.2 Criar Makefile na raiz
|
|
1946
|
+
generateDockerComposeAndDbInit(projectRoot, config);
|
|
1947
|
+
// 3. Gerar Makefile na raiz
|
|
2018
1948
|
console.log('Gerando arquivo Makefile na raiz...');
|
|
2019
1949
|
const backendServiceName = isMicroservices ? 'api-gateway' : 'backend';
|
|
2020
1950
|
const makefileContent = `.PHONY: up down restart build logs ps clean backend-shell frontend-shell db-shell dev-up dev-down dev-restart dev-build dev-logs dev-ps dev-clean help
|
|
@@ -2120,17 +2050,100 @@ db-shell:
|
|
|
2120
2050
|
catch (err) {
|
|
2121
2051
|
console.warn('[Aviso] Falha ao rodar "npm install" no frontend. Instale as dependências manualmente.');
|
|
2122
2052
|
}
|
|
2123
|
-
// 5. Inicializar
|
|
2053
|
+
// 5. Inicializar componentes de UI Spartan
|
|
2124
2054
|
console.log('Inicializando componentes de UI locais...');
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2055
|
+
initUi(frontendDest, config);
|
|
2056
|
+
console.log(`\n\x1b[32m[SUCESSO] Projeto "${projectName}" gerado com sucesso!\x1b[0m`);
|
|
2057
|
+
}
|
|
2058
|
+
catch (err) {
|
|
2059
|
+
console.error('[Erro] Falha ao gerar o projeto:', err);
|
|
2060
|
+
process.exit(1);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
// Comando new-project
|
|
2064
|
+
program
|
|
2065
|
+
.command('new-project <projectName>')
|
|
2066
|
+
.description('Cria um novo diretório de projeto e inicializa o devstroupe.config.ts')
|
|
2067
|
+
.option('--microservices', 'Cria o projeto utilizando arquitetura de microsserviços')
|
|
2068
|
+
.action((projectName, options) => {
|
|
2069
|
+
const projectRoot = path.resolve(process.cwd(), projectName);
|
|
2070
|
+
if (fs.existsSync(projectRoot) && fs.readdirSync(projectRoot).length > 0) {
|
|
2071
|
+
console.error(`[Erro] O diretório "${projectName}" já existe e não está vazio.`);
|
|
2072
|
+
process.exit(1);
|
|
2073
|
+
}
|
|
2074
|
+
const isMicroservices = !!options.microservices;
|
|
2075
|
+
console.log(`Inicializando novo projeto "${projectName}" em: ${projectRoot}...`);
|
|
2076
|
+
try {
|
|
2077
|
+
// 1. Criar diretório de destino
|
|
2078
|
+
fs.ensureDirSync(projectRoot);
|
|
2079
|
+
// 2. Criar devstroupe.config.ts padrão
|
|
2080
|
+
console.log('Gerando arquivo devstroupe.config.ts...');
|
|
2081
|
+
const configPath = path.join(projectRoot, 'devstroupe.config.ts');
|
|
2082
|
+
const configContent = `export default {
|
|
2083
|
+
projectName: '${projectName}',
|
|
2084
|
+
backendType: '${isMicroservices ? 'microservices' : 'monolith'}',
|
|
2085
|
+
backendPath: './backend',
|
|
2086
|
+
frontendPath: './frontend',
|
|
2087
|
+
designSystem: {
|
|
2088
|
+
provider: 'spartan',
|
|
2089
|
+
spartanStyle: 'vega',
|
|
2090
|
+
spartanTheme: 'neutral',
|
|
2091
|
+
installAllPrimitives: true,
|
|
2092
|
+
includePlayground: true
|
|
2093
|
+
},
|
|
2094
|
+
entities: [
|
|
2095
|
+
{
|
|
2096
|
+
name: 'company',
|
|
2097
|
+
label: 'Empresa',
|
|
2098
|
+
tableName: 'companies',
|
|
2099
|
+
properties: [
|
|
2100
|
+
{ name: 'name', type: 'string', required: true, label: 'Nome' },
|
|
2101
|
+
{
|
|
2102
|
+
name: 'cabins', type: 'relationship', required: false, label: 'Cabines',
|
|
2103
|
+
relation: {
|
|
2104
|
+
target: 'cabin', kind: 'one-to-many', foreignKey: 'company_id',
|
|
2105
|
+
inverseSide: 'company', control: 'table', embedded: true,
|
|
2106
|
+
columns: ['name', 'code', 'is_active'], lookup: { pageSize: 10 }
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
],
|
|
2110
|
+
crud: { generateFrontend: true, generateBackend: true }
|
|
2111
|
+
},
|
|
2112
|
+
{
|
|
2113
|
+
name: 'cabin',
|
|
2114
|
+
label: 'Cabine',
|
|
2115
|
+
tableName: 'cabins',
|
|
2116
|
+
properties: [
|
|
2117
|
+
{ name: 'name', type: 'string', required: true, label: 'Nome' },
|
|
2118
|
+
{ name: 'code', type: 'string', required: false, label: 'Código' },
|
|
2119
|
+
{
|
|
2120
|
+
name: 'company_id', type: 'relationship', required: true, label: 'Empresa',
|
|
2121
|
+
relation: {
|
|
2122
|
+
target: 'company', kind: 'many-to-one', relationName: 'company',
|
|
2123
|
+
inverseSide: 'cabins', control: 'select', displayField: 'name', onDelete: 'CASCADE',
|
|
2124
|
+
lookup: { mode: 'remote', pageSize: 20, minSearchLength: 0 }
|
|
2125
|
+
}
|
|
2126
|
+
},
|
|
2127
|
+
{ name: 'is_active', type: 'boolean', required: true, label: 'Ativo', default: true }
|
|
2128
|
+
],
|
|
2129
|
+
crud: {
|
|
2130
|
+
generateFrontend: true,
|
|
2131
|
+
generateBackend: true
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
]
|
|
2135
|
+
};
|
|
2136
|
+
`;
|
|
2137
|
+
fs.writeFileSync(configPath, configContent, 'utf-8');
|
|
2138
|
+
console.log(`\n\x1b[32m[SUCESSO] Projeto "${projectName}" inicializado com sucesso!\x1b[0m`);
|
|
2128
2139
|
console.log(`Para começar:`);
|
|
2129
|
-
console.log(` 1.
|
|
2130
|
-
console.log(` 2.
|
|
2140
|
+
console.log(` 1. Acesse a pasta do projeto: cd ${projectName}`);
|
|
2141
|
+
console.log(` 2. Ajuste suas entidades e configurações no arquivo: devstroupe.config.ts`);
|
|
2142
|
+
console.log(` 3. Execute o comando a seguir para gerar toda a estrutura física (frontend, backend, Docker):`);
|
|
2143
|
+
console.log(` devstroupe generate project`);
|
|
2131
2144
|
}
|
|
2132
2145
|
catch (err) {
|
|
2133
|
-
console.error('[Erro] Falha ao
|
|
2146
|
+
console.error('[Erro] Falha ao inicializar o novo projeto:', err);
|
|
2134
2147
|
process.exit(1);
|
|
2135
2148
|
}
|
|
2136
2149
|
});
|
|
@@ -80,16 +80,25 @@ function nestMigrationTemplate(entity, entities, timestamp, properties = entity.
|
|
|
80
80
|
const relationColumns = relationships
|
|
81
81
|
.filter((relationship) => (0, relationships_1.isSingularRelationship)(relationship.kind))
|
|
82
82
|
.map((relationship) => ` { name: '${relationship.foreignKey}', type: 'int', isNullable: ${!relationship.property.required}, isUnique: ${relationship.kind === 'one-to-one'} }`);
|
|
83
|
-
|
|
83
|
+
// Gera SQL idempotente para cada FK: DROP IF EXISTS + ADD CONSTRAINT
|
|
84
|
+
const foreignKeysSql = relationships
|
|
84
85
|
.filter((relationship) => (0, relationships_1.isSingularRelationship)(relationship.kind))
|
|
85
|
-
.map((relationship) =>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
.map((relationship) => {
|
|
87
|
+
const fkName = `FK_${tableName}_${relationship.foreignKey}`;
|
|
88
|
+
const refTable = targetTable(relationship.target, entities);
|
|
89
|
+
return [
|
|
90
|
+
` await queryRunner.query(\`ALTER TABLE \\\`${tableName}\\\` DROP FOREIGN KEY IF EXISTS \\\`${fkName}\\\`\`);`,
|
|
91
|
+
` await queryRunner.query(\`ALTER TABLE \\\`${tableName}\\\` ADD CONSTRAINT \\\`${fkName}\\\` FOREIGN KEY (\\\`${relationship.foreignKey}\\\`) REFERENCES \\\`${refTable}\\\`(\\\`${relationship.valueField}\\\`) ON DELETE ${relationship.onDelete}\`);`,
|
|
92
|
+
].join('\n');
|
|
93
|
+
})
|
|
94
|
+
.join('\n');
|
|
95
|
+
const dropForeignKeysSql = relationships
|
|
96
|
+
.filter((relationship) => (0, relationships_1.isSingularRelationship)(relationship.kind))
|
|
97
|
+
.map((relationship) => {
|
|
98
|
+
const fkName = `FK_${tableName}_${relationship.foreignKey}`;
|
|
99
|
+
return ` await queryRunner.query(\`ALTER TABLE \\\`${tableName}\\\` DROP FOREIGN KEY IF EXISTS \\\`${fkName}\\\`\`);`;
|
|
100
|
+
})
|
|
101
|
+
.join('\n');
|
|
93
102
|
const junctions = relationships
|
|
94
103
|
.filter((relationship) => relationship.kind === 'many-to-many' && relationship.owner)
|
|
95
104
|
.map((relationship) => {
|
|
@@ -154,7 +163,7 @@ function nestMigrationTemplate(entity, entities, timestamp, properties = entity.
|
|
|
154
163
|
return ` await queryRunner.query('ALTER TABLE \`${tableName}\` DROP CONSTRAINT \`${checkName}\`');`;
|
|
155
164
|
}).join('\n')
|
|
156
165
|
: '';
|
|
157
|
-
return `import { MigrationInterface, QueryRunner, Table,
|
|
166
|
+
return `import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
|
|
158
167
|
|
|
159
168
|
export class ${className} implements MigrationInterface {
|
|
160
169
|
name = '${className}';
|
|
@@ -166,10 +175,7 @@ export class ${className} implements MigrationInterface {
|
|
|
166
175
|
{ name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment' }${scalarColumns.length + relationColumns.length > 0 ? ',\n' : ''}${[...scalarColumns, ...relationColumns].join(',\n')}
|
|
167
176
|
]
|
|
168
177
|
}), true);
|
|
169
|
-
${
|
|
170
|
-
await queryRunner.createForeignKeys('${tableName}', [
|
|
171
|
-
${foreignKeys}
|
|
172
|
-
]);` : ''}
|
|
178
|
+
${foreignKeysSql ? `\n${foreignKeysSql}` : ''}
|
|
173
179
|
${tableIndexes ? `\n${tableIndexes}` : ''}
|
|
174
180
|
${checkConstraints ? `\n${checkConstraints}` : ''}
|
|
175
181
|
${junctions.map((junction) => `
|
|
@@ -177,7 +183,7 @@ ${junction.up}`).join('\n')}
|
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
async down(queryRunner: QueryRunner): Promise<void> {
|
|
180
|
-
${dropChecks ? `${dropChecks}\n` : ''}${dropIndexes ? `${dropIndexes}\n` : ''}${junctions.slice().reverse().map((junction) => ` await queryRunner.dropTable('${junction.tableName}', true);`).join('\n')}${junctions.length > 0 ? '\n' : ''} await queryRunner.dropTable('${tableName}', true);
|
|
186
|
+
${dropChecks ? `${dropChecks}\n` : ''}${dropIndexes ? `${dropIndexes}\n` : ''}${dropForeignKeysSql ? `${dropForeignKeysSql}\n` : ''}${junctions.slice().reverse().map((junction) => ` await queryRunner.dropTable('${junction.tableName}', true);`).join('\n')}${junctions.length > 0 ? '\n' : ''} await queryRunner.dropTable('${tableName}', true);
|
|
181
187
|
}
|
|
182
188
|
}
|
|
183
189
|
`;
|
|
@@ -149,8 +149,9 @@ function assertValidTypeScript(source) {
|
|
|
149
149
|
strict_1.default.match(companyMigration, /name: 'companies'/);
|
|
150
150
|
strict_1.default.doesNotMatch(companyMigration, /FK_companies_cabins/);
|
|
151
151
|
strict_1.default.match(cabinMigration, /name: 'company_id', type: 'int'/);
|
|
152
|
-
strict_1.default.match(cabinMigration, /
|
|
153
|
-
strict_1.default.match(cabinMigration, /
|
|
152
|
+
strict_1.default.match(cabinMigration, /ADD CONSTRAINT/);
|
|
153
|
+
strict_1.default.match(cabinMigration, /FK_cabins_company_id/);
|
|
154
|
+
strict_1.default.match(cabinMigration, /ON DELETE CASCADE/);
|
|
154
155
|
strict_1.default.match(dataSource, /migrations: \[path\.join/);
|
|
155
156
|
strict_1.default.match(dataSource, /synchronize: false/);
|
|
156
157
|
assertValidTypeScript(companyMigration);
|
|
@@ -8,11 +8,12 @@ import { CommonModule } from '@angular/common';
|
|
|
8
8
|
import { HlmButton } from '@spartan-ng/helm/button';
|
|
9
9
|
import { HlmInput } from '@spartan-ng/helm/input';
|
|
10
10
|
import { NgIconComponent } from '@ng-icons/core';
|
|
11
|
+
import type { ThemeMode } from '../../../core/services/theme.service';
|
|
11
12
|
|
|
12
13
|
export interface IUserProfile {
|
|
13
14
|
name: string;
|
|
14
15
|
avatar?: string;
|
|
15
|
-
role?: string
|
|
16
|
+
role?: any; // pode ser string ou { name: string, permissions: string[] }
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
@Component({
|
|
@@ -28,16 +29,35 @@ export class DevkitHeaderComponent {
|
|
|
28
29
|
@Input() user: IUserProfile | null = null;
|
|
29
30
|
@Input() isSidebarCollapsed = false;
|
|
30
31
|
@Input() isDarkMode = false;
|
|
32
|
+
@Input() themeMode: ThemeMode = 'auto';
|
|
31
33
|
|
|
32
34
|
@Output() toggleSidebar = new EventEmitter<void>();
|
|
33
35
|
@Output() logout = new EventEmitter<void>();
|
|
34
36
|
@Output() toggleTheme = new EventEmitter<void>();
|
|
35
37
|
@Output() openProfile = new EventEmitter<void>();
|
|
38
|
+
@Output() setThemeMode = new EventEmitter<ThemeMode>();
|
|
36
39
|
|
|
37
40
|
isProfileDropdownOpen = false;
|
|
41
|
+
isThemeDropdownOpen = false;
|
|
42
|
+
|
|
43
|
+
get roleName(): string {
|
|
44
|
+
if (!this.user?.role) return '';
|
|
45
|
+
return typeof this.user.role === 'object' ? (this.user.role.name ?? '') : this.user.role;
|
|
46
|
+
}
|
|
38
47
|
|
|
39
48
|
toggleProfileDropdown(): void {
|
|
40
49
|
this.isProfileDropdownOpen = !this.isProfileDropdownOpen;
|
|
50
|
+
this.isThemeDropdownOpen = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
toggleThemeDropdown(): void {
|
|
54
|
+
this.isThemeDropdownOpen = !this.isThemeDropdownOpen;
|
|
55
|
+
this.isProfileDropdownOpen = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
onSelectTheme(mode: ThemeMode): void {
|
|
59
|
+
this.isThemeDropdownOpen = false;
|
|
60
|
+
this.setThemeMode.emit(mode);
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
onLogoutClick(): void {
|
|
@@ -53,6 +73,18 @@ export class DevkitHeaderComponent {
|
|
|
53
73
|
onToggleTheme(): void {
|
|
54
74
|
this.toggleTheme.emit();
|
|
55
75
|
}
|
|
76
|
+
|
|
77
|
+
get themeIcon(): string {
|
|
78
|
+
if (this.themeMode === 'light') return 'lucideSun';
|
|
79
|
+
if (this.themeMode === 'dark') return 'lucideMoon';
|
|
80
|
+
return 'lucideMonitor';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get themeLabel(): string {
|
|
84
|
+
if (this.themeMode === 'light') return 'Light';
|
|
85
|
+
if (this.themeMode === 'dark') return 'Dark';
|
|
86
|
+
return 'Auto';
|
|
87
|
+
}
|
|
56
88
|
}
|
|
57
89
|
`,
|
|
58
90
|
html: `<header class="dt-header">
|
|
@@ -70,18 +102,35 @@ export class DevkitHeaderComponent {
|
|
|
70
102
|
|
|
71
103
|
<div class="flex items-center space-x-3">
|
|
72
104
|
|
|
73
|
-
<!--
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
<!-- Seletor de Tema (Light / Dark / Auto) -->
|
|
106
|
+
<div class="relative">
|
|
107
|
+
<button
|
|
108
|
+
hlmBtn
|
|
109
|
+
variant="ghost"
|
|
110
|
+
size="icon"
|
|
111
|
+
class="dt-header-action"
|
|
112
|
+
[title]="'Tema: ' + themeLabel"
|
|
113
|
+
(click)="toggleThemeDropdown()"
|
|
114
|
+
>
|
|
115
|
+
<ng-icon [name]="themeIcon" class="size-5"></ng-icon>
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
<!-- Dropdown de Tema -->
|
|
119
|
+
<div *ngIf="isThemeDropdownOpen" class="dt-theme-dropdown">
|
|
120
|
+
<button class="dt-theme-option" [class.dt-theme-option--active]="themeMode === 'light'" (click)="onSelectTheme('light')">
|
|
121
|
+
<ng-icon name="lucideSun" class="size-4 mr-2"></ng-icon>
|
|
122
|
+
Light
|
|
123
|
+
</button>
|
|
124
|
+
<button class="dt-theme-option" [class.dt-theme-option--active]="themeMode === 'dark'" (click)="onSelectTheme('dark')">
|
|
125
|
+
<ng-icon name="lucideMoon" class="size-4 mr-2"></ng-icon>
|
|
126
|
+
Dark
|
|
127
|
+
</button>
|
|
128
|
+
<button class="dt-theme-option" [class.dt-theme-option--active]="themeMode === 'auto'" (click)="onSelectTheme('auto')">
|
|
129
|
+
<ng-icon name="lucideMonitor" class="size-4 mr-2"></ng-icon>
|
|
130
|
+
Auto
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
85
134
|
|
|
86
135
|
<!-- Notificações -->
|
|
87
136
|
<button *ngIf="showNotifications" hlmBtn variant="ghost" size="icon" class="dt-header-action relative" title="Notificações">
|
|
@@ -103,7 +152,7 @@ export class DevkitHeaderComponent {
|
|
|
103
152
|
</ng-template>
|
|
104
153
|
<div class="hidden md:flex flex-col text-left">
|
|
105
154
|
<span class="text-sm font-semibold leading-none text-foreground">{{ user.name }}</span>
|
|
106
|
-
<span *ngIf="
|
|
155
|
+
<span *ngIf="roleName" class="text-[10px] font-medium leading-none mt-1 text-muted-foreground">{{ roleName }}</span>
|
|
107
156
|
</div>
|
|
108
157
|
</button>
|
|
109
158
|
|
|
@@ -111,7 +160,7 @@ export class DevkitHeaderComponent {
|
|
|
111
160
|
<div *ngIf="isProfileDropdownOpen" class="dt-header-dropdown">
|
|
112
161
|
<div class="px-4 py-3 border-b border-border">
|
|
113
162
|
<p class="text-sm font-bold text-foreground">{{ user.name }}</p>
|
|
114
|
-
<p *ngIf="
|
|
163
|
+
<p *ngIf="roleName" class="text-xs text-muted-foreground mt-0.5">{{ roleName }}</p>
|
|
115
164
|
</div>
|
|
116
165
|
<button hlmBtn variant="ghost" (click)="onOpenProfile()" class="dt-header-dropdown-item">
|
|
117
166
|
<ng-icon name="lucideUser" class="size-4 mr-2"></ng-icon>
|
|
@@ -180,6 +229,46 @@ export class DevkitHeaderComponent {
|
|
|
180
229
|
width: 280px;
|
|
181
230
|
}
|
|
182
231
|
|
|
232
|
+
.dt-theme-dropdown {
|
|
233
|
+
position: absolute;
|
|
234
|
+
right: 0;
|
|
235
|
+
top: calc(100% + 8px);
|
|
236
|
+
width: 140px;
|
|
237
|
+
background-color: var(--popover);
|
|
238
|
+
color: var(--popover-foreground);
|
|
239
|
+
border: 1px solid var(--border);
|
|
240
|
+
border-radius: var(--radius);
|
|
241
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
242
|
+
z-index: 100;
|
|
243
|
+
display: flex;
|
|
244
|
+
flex-direction: column;
|
|
245
|
+
padding: 4px 0;
|
|
246
|
+
font-family: var(--font-family);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.dt-theme-option {
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
width: 100%;
|
|
253
|
+
padding: 9px 14px;
|
|
254
|
+
background: transparent;
|
|
255
|
+
border: none;
|
|
256
|
+
font-size: 13px;
|
|
257
|
+
color: var(--popover-foreground);
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
font-family: var(--font-family);
|
|
260
|
+
transition: background-color 0.15s ease-in-out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.dt-theme-option:hover {
|
|
264
|
+
background-color: var(--muted);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.dt-theme-option--active {
|
|
268
|
+
color: var(--primary);
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
}
|
|
271
|
+
|
|
183
272
|
.dt-header-dropdown {
|
|
184
273
|
position: absolute;
|
|
185
274
|
right: 0;
|
|
@@ -32,6 +32,7 @@ export class MainLayoutComponent implements OnInit {
|
|
|
32
32
|
menuItems: INavigationItem[] = APP_NAVIGATION;
|
|
33
33
|
currentUser$ = this.authService.currentUser$;
|
|
34
34
|
isDark$ = this.themeService.isDark$;
|
|
35
|
+
themeMode$ = this.themeService.themeMode$;
|
|
35
36
|
|
|
36
37
|
brandLogo = '${brandLogo}';
|
|
37
38
|
brandLogoCollapsed = '${brandLogoCollapsed}';
|
|
@@ -53,6 +54,10 @@ export class MainLayoutComponent implements OnInit {
|
|
|
53
54
|
this.themeService.toggle();
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
onSetThemeMode(mode: 'light' | 'dark' | 'auto'): void {
|
|
58
|
+
this.themeService.setMode(mode);
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
onOpenProfile(): void {
|
|
57
62
|
this.router.navigate(['/profile']);
|
|
58
63
|
}
|
|
@@ -87,9 +92,11 @@ export class MainLayoutComponent implements OnInit {
|
|
|
87
92
|
[user]="currentUser$ | async"
|
|
88
93
|
[isSidebarCollapsed]="isCollapsed"
|
|
89
94
|
[isDarkMode]="(isDark$ | async) ?? false"
|
|
95
|
+
[themeMode]="(themeMode$ | async) ?? 'auto'"
|
|
90
96
|
(toggleSidebar)="isCollapsed = !isCollapsed"
|
|
91
97
|
(logout)="onLogout()"
|
|
92
98
|
(toggleTheme)="onToggleTheme()"
|
|
99
|
+
(setThemeMode)="onSetThemeMode($event)"
|
|
93
100
|
(openProfile)="onOpenProfile()"
|
|
94
101
|
></devkit-header>
|
|
95
102
|
|
|
@@ -5,42 +5,57 @@ function devkitThemeServiceTemplate() {
|
|
|
5
5
|
return `import { Injectable } from '@angular/core';
|
|
6
6
|
import { BehaviorSubject } from 'rxjs';
|
|
7
7
|
|
|
8
|
+
export type ThemeMode = 'light' | 'dark' | 'auto';
|
|
9
|
+
|
|
8
10
|
const THEME_KEY = 'devkit-theme';
|
|
9
11
|
|
|
10
12
|
@Injectable({
|
|
11
13
|
providedIn: 'root'
|
|
12
14
|
})
|
|
13
15
|
export class ThemeService {
|
|
16
|
+
private modeSubject = new BehaviorSubject<ThemeMode>('auto');
|
|
14
17
|
private isDarkSubject = new BehaviorSubject<boolean>(false);
|
|
18
|
+
|
|
19
|
+
themeMode$ = this.modeSubject.asObservable();
|
|
15
20
|
isDark$ = this.isDarkSubject.asObservable();
|
|
16
21
|
|
|
17
|
-
get isDark(): boolean {
|
|
18
|
-
|
|
19
|
-
}
|
|
22
|
+
get isDark(): boolean { return this.isDarkSubject.getValue(); }
|
|
23
|
+
get mode(): ThemeMode { return this.modeSubject.getValue(); }
|
|
20
24
|
|
|
21
25
|
constructor() {
|
|
22
26
|
this.loadTheme();
|
|
27
|
+
window.matchMedia?.('(prefers-color-scheme: dark)')
|
|
28
|
+
.addEventListener('change', () => {
|
|
29
|
+
if (this.mode === 'auto') this.applyMode('auto');
|
|
30
|
+
});
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
/** Cicla entre light → dark → auto */
|
|
25
34
|
toggle(): void {
|
|
26
|
-
this.
|
|
35
|
+
const next: ThemeMode = this.mode === 'light' ? 'dark' : this.mode === 'dark' ? 'auto' : 'light';
|
|
36
|
+
this.setMode(next);
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
setMode(mode: ThemeMode): void {
|
|
40
|
+
this.modeSubject.next(mode);
|
|
41
|
+
localStorage.setItem(THEME_KEY, mode);
|
|
42
|
+
this.applyMode(mode);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @deprecated use setMode */
|
|
46
|
+
setDark(dark: boolean): void { this.setMode(dark ? 'dark' : 'light'); }
|
|
47
|
+
|
|
48
|
+
private applyMode(mode: ThemeMode): void {
|
|
49
|
+
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
|
|
50
|
+
const dark = mode === 'dark' || (mode === 'auto' && prefersDark);
|
|
30
51
|
this.isDarkSubject.next(dark);
|
|
31
|
-
|
|
32
|
-
if (dark) {
|
|
33
|
-
document.documentElement.classList.add('dark');
|
|
34
|
-
} else {
|
|
35
|
-
document.documentElement.classList.remove('dark');
|
|
36
|
-
}
|
|
52
|
+
document.documentElement.classList.toggle('dark', dark);
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
private loadTheme(): void {
|
|
40
|
-
const saved = localStorage.getItem(THEME_KEY);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.setDark(dark);
|
|
56
|
+
const saved = (localStorage.getItem(THEME_KEY) ?? 'auto') as ThemeMode;
|
|
57
|
+
this.modeSubject.next(saved);
|
|
58
|
+
this.applyMode(saved);
|
|
44
59
|
}
|
|
45
60
|
}
|
|
46
61
|
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devstroupe/devkit-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"description": "DevsTroupe Development Kit CLI — scaffold NestJS+Angular projects, inject Spartan UI, generate CRUDs and audit architectural governance",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"commander": "^12.0.0",
|
|
38
38
|
"fs-extra": "^11.2.0",
|
|
39
|
-
"@devstroupe/devkit-core": "1.1.
|
|
39
|
+
"@devstroupe/devkit-core": "1.1.10"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/fs-extra": "^11.0.4",
|