feat: config-driven реестр на реальных данных SP

This commit is contained in:
Тимур Городецкий
2026-08-17 11:23:57 +05:00
commit 31ef6fdbb9
99 changed files with 33739 additions and 0 deletions
+321
View File
@@ -0,0 +1,321 @@
require('@rushstack/eslint-config/patch/modern-module-resolution');
module.exports = {
extends: ['@microsoft/eslint-config-spfx/lib/profiles/react'],
parserOptions: { tsconfigRootDir: __dirname },
overrides: [
{
files: ['*.ts', '*.tsx'],
parser: '@typescript-eslint/parser',
'parserOptions': {
'project': './tsconfig.json',
'ecmaVersion': 2018,
'sourceType': 'module'
},
rules: {
// Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin
'@rushstack/no-new-null': 1,
// Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin
'@rushstack/hoist-jest-mock': 1,
// Require chunk names for dynamic imports in SPFx projects. https://www.npmjs.com/package/@rushstack/eslint-plugin
'@rushstack/import-requires-chunk-name': 1,
// Ensure that React components rendered with ReactDOM.render() are unmounted with ReactDOM.unmountComponentAtNode(). https://www.npmjs.com/package/@rushstack/eslint-plugin
'@rushstack/pair-react-dom-render-unmount': 1,
// Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security
'@rushstack/security/no-unsafe-regexp': 1,
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
'@typescript-eslint/adjacent-overload-signatures': 1,
// RATIONALE: Code is more readable when the type of every variable is immediately obvious.
// Even if the compiler may be able to infer a type, this inference will be unavailable
// to a person who is reviewing a GitHub diff. This rule makes writing code harder,
// but writing code is a much less important activity than reading it.
//
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
'@typescript-eslint/explicit-function-return-type': [
1,
{
'allowExpressions': true,
'allowTypedFunctionExpressions': true,
'allowHigherOrderFunctions': false
}
],
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
// Rationale to disable: although this is a recommended rule, it is up to dev to select coding style.
// Set to 1 (warning) or 2 (error) to enable.
'@typescript-eslint/explicit-member-accessibility': 0,
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
'@typescript-eslint/no-array-constructor': 1,
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
//
// RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript.
// This rule should be suppressed only in very special cases such as JSON.stringify()
// where the type really can be anything. Even if the type is flexible, another type
// may be more appropriate such as "unknown", "{}", or "Record<k,V>".
'@typescript-eslint/no-explicit-any': 1,
// RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch()
// handler. Thus wherever a Promise arises, the code must either append a catch handler,
// or else return the object to a caller (who assumes this responsibility). Unterminated
// promise chains are a serious issue. Besides causing errors to be silently ignored,
// they can also cause a NodeJS process to terminate unexpectedly.
'@typescript-eslint/no-floating-promises': 2,
// RATIONALE: Catches a common coding mistake.
'@typescript-eslint/no-for-in-array': 2,
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
'@typescript-eslint/no-misused-new': 2,
// RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks
// a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler
// optimizations. If you are declaring loose functions/variables, it's better to make them
// static members of a class, since classes support property getters and their private
// members are accessible by unit tests. Also, the exercise of choosing a meaningful
// class name tends to produce more discoverable APIs: for example, search+replacing
// the function "reverse()" is likely to return many false matches, whereas if we always
// write "Text.reverse()" is more unique. For large scale organization, it's recommended
// to decompose your code into separate NPM packages, which ensures that component
// dependencies are tracked more conscientiously.
//
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
'@typescript-eslint/no-namespace': [
1,
{
'allowDeclarations': false,
'allowDefinitionFiles': false
}
],
// RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)"
// that avoids the effort of declaring "title" as a field. This TypeScript feature makes
// code easier to write, but arguably sacrifices readability: In the notes for
// "@typescript-eslint/member-ordering" we pointed out that fields are central to
// a class's design, so we wouldn't want to bury them in a constructor signature
// just to save some typing.
//
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
// Set to 1 (warning) or 2 (error) to enable the rule
'@typescript-eslint/parameter-properties': 0,
// RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code
// may impact performance.
//
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
'@typescript-eslint/no-unused-vars': [
1,
{
'vars': 'all',
// Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code,
// the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures
// that are overriding a base class method or implementing an interface.
'args': 'none'
}
],
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
'@typescript-eslint/no-use-before-define': [
2,
{
'functions': false,
'classes': true,
'variables': true,
'enums': true,
'typedefs': true
}
],
// Disallows require statements except in import statements.
// In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports.
'@typescript-eslint/no-var-requires': 'error',
// RATIONALE: The "module" keyword is deprecated except when describing legacy libraries.
//
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
'@typescript-eslint/prefer-namespace-keyword': 1,
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
// Rationale to disable: it's up to developer to decide if he wants to add type annotations
// Set to 1 (warning) or 2 (error) to enable the rule
'@typescript-eslint/no-inferrable-types': 0,
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
// Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios
'@typescript-eslint/no-empty-interface': 0,
// RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake.
'accessor-pairs': 1,
// RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking.
'dot-notation': [
1,
{
'allowPattern': '^_'
}
],
// RATIONALE: Catches code that is likely to be incorrect
'eqeqeq': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'for-direction': 1,
// RATIONALE: Catches a common coding mistake.
'guard-for-in': 2,
// RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time
// to split up your code.
'max-lines': ['warn', { max: 2000 }],
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-async-promise-executor': 2,
// RATIONALE: Deprecated language feature.
'no-caller': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-compare-neg-zero': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-cond-assign': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-constant-condition': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-control-regex': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-debugger': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-delete-var': 2,
// RATIONALE: Catches code that is likely to be incorrect
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-duplicate-case': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-empty': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-empty-character-class': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-empty-pattern': 1,
// RATIONALE: Eval is a security concern and a performance concern.
'no-eval': 1,
// RATIONALE: Catches code that is likely to be incorrect
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-ex-assign': 2,
// RATIONALE: System types are global and should not be tampered with in a scalable code base.
// If two different libraries (or two versions of the same library) both try to modify
// a type, only one of them can win. Polyfills are acceptable because they implement
// a standardized interoperable contract, but polyfills are generally coded in plain
// JavaScript.
'no-extend-native': 1,
// Disallow unnecessary labels
'no-extra-label': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-fallthrough': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-func-assign': 1,
// RATIONALE: Catches a common coding mistake.
'no-implied-eval': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-invalid-regexp': 2,
// RATIONALE: Catches a common coding mistake.
'no-label-var': 2,
// RATIONALE: Eliminates redundant code.
'no-lone-blocks': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-misleading-character-class': 2,
// RATIONALE: Catches a common coding mistake.
'no-multi-str': 2,
// RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to
// a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()",
// or else implies that the constructor is doing nontrivial computations, which is often
// a poor class design.
'no-new': 1,
// RATIONALE: Obsolete language feature that is deprecated.
'no-new-func': 2,
// RATIONALE: Obsolete language feature that is deprecated.
'no-new-object': 2,
// RATIONALE: Obsolete notation.
'no-new-wrappers': 1,
// RATIONALE: Catches code that is likely to be incorrect
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-octal': 2,
// RATIONALE: Catches code that is likely to be incorrect
'no-octal-escape': 2,
// RATIONALE: Catches code that is likely to be incorrect
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-regex-spaces': 2,
// RATIONALE: Catches a common coding mistake.
'no-return-assign': 2,
// RATIONALE: Security risk.
'no-script-url': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-self-assign': 2,
// RATIONALE: Catches a common coding mistake.
'no-self-compare': 2,
// RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use
// commas to create compound expressions. In general code is more readable if each
// step is split onto a separate line. This also makes it easier to set breakpoints
// in the debugger.
'no-sequences': 1,
// RATIONALE: Catches code that is likely to be incorrect
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-shadow-restricted-names': 2,
// RATIONALE: Obsolete language feature that is deprecated.
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-sparse-arrays': 2,
// RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception,
// such flexibility adds pointless complexity, by requiring every catch block to test
// the type of the object that it receives. Whereas if catch blocks can always assume
// that their object implements the "Error" contract, then the code is simpler, and
// we generally get useful additional information like a call stack.
'no-throw-literal': 2,
// RATIONALE: Catches a common coding mistake.
'no-unmodified-loop-condition': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-unsafe-finally': 2,
// RATIONALE: Catches a common coding mistake.
'no-unused-expressions': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-unused-labels': 1,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-useless-catch': 1,
// RATIONALE: Avoids a potential performance problem.
'no-useless-concat': 1,
// RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior.
// Always use "let" or "const" instead.
//
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
'no-var': 2,
// RATIONALE: Generally not needed in modern code.
'no-void': 1,
// RATIONALE: Obsolete language feature that is deprecated.
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'no-with': 2,
// RATIONALE: Makes logic easier to understand, since constants always have a known value
// @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js
'prefer-const': 1,
// RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused.
'promise/param-names': 2,
// RATIONALE: Catches code that is likely to be incorrect
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'require-atomic-updates': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'require-yield': 1,
// "Use strict" is redundant when using the TypeScript compiler.
'strict': [
2,
'never'
],
// RATIONALE: Catches code that is likely to be incorrect
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
'use-isnan': 2,
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
// Set to 1 (warning) or 2 (error) to enable.
// Rationale to disable: !!{}
'no-extra-boolean-cast': 0,
// ====================================================================
// @microsoft/eslint-plugin-spfx
// ====================================================================
'@microsoft/spfx/no-require-ensure': 2,
}
},
{
// For unit tests, we can be a little bit less strict. The settings below revise the
// defaults specified in the extended configurations, as well as above.
files: [
// Test files
'*.test.ts',
'*.test.tsx',
'*.spec.ts',
'*.spec.tsx',
// Facebook convention
'**/__mocks__/*.ts',
'**/__mocks__/*.tsx',
'**/__tests__/*.ts',
'**/__tests__/*.tsx',
// Microsoft convention
'**/test/*.ts',
'**/test/*.tsx'
],
rules: {}
}
]
};
+7
View File
@@ -0,0 +1,7 @@
node_modules/
lib/
dist/
temp/
*.sppkg
.heft/
sharepoint/solution/
+22
View File
@@ -0,0 +1,22 @@
# Ignore everything by default
**
# Use negative patterns to bring back the specific things we want to publish
!/bin/**
!/dist/**
!/EULA/**
!/lib/**
!ThirdPartyNotice.txt
# Ignore certain files in the above folders
/dist/*.stats.*
/dist/**/*.js.map
/lib/**/*.js.map
/lib/**/test/**
# NOTE: These don't need to be specified, because NPM includes them automatically.
#
# package.json
# README (and its variants)
# CHANGELOG (and its variants)
# LICENSE / LICENCE
+1
View File
@@ -0,0 +1 @@
@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/
+23
View File
@@ -0,0 +1,23 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Hosted workbench",
"type": "msedge",
"request": "launch",
"url": "https://{tenantDomain}/_layouts/workbench.aspx",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///.././src/*": "${webRoot}/src/*",
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222",
"-incognito"
]
}
]
}
+14
View File
@@ -0,0 +1,14 @@
// Place your settings in this file to overwrite default and user settings.
{
// Configure glob patterns for excluding files and folders in the file explorer.
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/bower_components": true,
"**/coverage": true,
"**/jest-output": true,
"**/lib-amd": true,
"src/**/*.scss.ts": true
},
"typescript.tsdk": ".\\node_modules\\typescript\\lib"
}
+19
View File
@@ -0,0 +1,19 @@
{
"@microsoft/generator-sharepoint": {
"useGulp": false,
"plusBeta": false,
"isCreatingSolution": true,
"nodeVersion": "22.22.2",
"sdksVersions": {},
"version": "1.22.2",
"libraryName": "ct-forms-portal",
"libraryId": "a1c3b00d-3ac8-449a-ad9d-6d0d522df14f",
"environment": "spo",
"packageManager": "npm",
"solutionName": "ct-forms-portal",
"solutionShortDescription": "ct-forms-portal description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}
+77
View File
@@ -0,0 +1,77 @@
# ct-forms-portal
## Summary
Short summary on functionality and used technologies.
[picture of the solution in action, if possible]
## Used SharePoint Framework Version
![version](https://img.shields.io/badge/version-1.22.2-green.svg)
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
> Any special pre-requisites?
## Solution
| Solution | Author(s) |
| ----------- | ------------------------------------------------------- |
| folder name | Author details (name, company, twitter alias with link) |
## Version history
| Version | Date | Comments |
| ------- | ---------------- | --------------- |
| 1.1 | March 10, 2021 | Update comment |
| 1.0 | January 29, 2021 | Initial release |
## Disclaimer
**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- `npm install -g @rushstack/heft`
- `npm install`
- `heft start`
> Include any additional steps as needed.
Other build commands can be listed using `heft --help`.
## Features
Description of the extension that expands upon high-level summary above.
This extension illustrates the following concepts:
- topic 1
- topic 2
- topic 3
> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance.
> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp.
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
- [Heft Documentation](https://heft.rushstack.io/)
+18
View File
@@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"ct-forms-portal-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/ctFormsPortal/CtFormsPortalWebPart.js",
"manifest": "./src/webparts/ctFormsPortal/CtFormsPortalWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"CtFormsPortalWebPartStrings": "lib/webparts/ctFormsPortal/loc/{locale}.js"
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "ct-forms-portal",
"accessKey": "<!-- ACCESS KEY -->"
}
+40
View File
@@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "ct-forms-portal-client-side-solution",
"id": "a1c3b00d-3ac8-449a-ad9d-6d0d522df14f",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.22.2"
},
"metadata": {
"shortDescription": {
"default": "ct-forms-portal description"
},
"longDescription": {
"default": "ct-forms-portal description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "ct-forms-portal Feature",
"description": "The feature that activates elements of the ct-forms-portal solution.",
"id": "5edbf144-12c3-4535-bb61-6417cdcc5f7d",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/ct-forms-portal.sppkg"
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@microsoft/spfx-web-build-rig"
}
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json",
"extends": "@microsoft/spfx-web-build-rig/profiles/default/config/sass.json"
}
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://tnsukz.sharepoint.com/_layouts/workbench.aspx"
}
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "@microsoft/spfx-web-build-rig/profiles/default/config/typescript.json",
"staticAssetsToCopy": {
"fileExtensions": [".resx", ".jpg", ".png", ".woff", ".eot", ".ttf", ".svg", ".gif"],
"includeGlobs": ["webparts/*/loc/*.js"]
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}
+315
View File
@@ -0,0 +1,315 @@
# =============================================================
# SPFx + Heft project bootstrap script (PowerShell)
# ВАРИАНТ С ПОДКЛЮЧЕНИЕМ ОБЩЕЙ UI-БИБЛИОТЕКИ @tnsu/ui-kit.
# Запустите из корня вашей ПУСТОЙ папки проекта:
# .\create-spfx-uikit.ps1
#
# Если запуск скриптов заблокирован, выполните сначала:
# Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
#
# Требование: у запускающего должен быть токен @tnsu в ~/.npmrc
# (см. README пакета tnsu-ui-kit) — иначе установка @tnsu/ui-kit не пройдёт.
# =============================================================
$ErrorActionPreference = "Stop"
# ---- Цвета вывода ----
function Info { param($msg) Write-Host "[INFO] $msg" -ForegroundColor Cyan }
function Success { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green }
function Warn { param($msg) Write-Host "[WARN] $msg" -ForegroundColor Yellow }
function Header { param($msg) Write-Host "`n$msg" -ForegroundColor White }
Header "================================================="
Header " SPFx + Heft — автоматическая инициализация"
Header " (с общей UI-библиотекой @tnsu/ui-kit)"
Header "=================================================`n"
# ---------------------------------------------------------
# 1. Проверка Node.js (SPFx 1.20+ требует Node 18+)
# ---------------------------------------------------------
Info "Проверка Node.js..."
try {
$nodeVersion = node -e "process.stdout.write(process.version)" 2>&1
} catch {
Warn "Node.js не найден. Установите Node 18 LTS: https://nodejs.org"
exit 1
}
$nodeMajor = [int]($nodeVersion.TrimStart('v').Split('.')[0])
if ($nodeMajor -lt 18) {
Warn "Обнаружен Node.js $nodeVersion. SPFx 1.20+ требует Node 18.x или выше."
Warn "Установите Node 18 LTS через nvm-windows или с https://nodejs.org"
exit 1
}
Success "Node.js $nodeVersion — подходит."
# ---------------------------------------------------------
# 2. Вспомогательная функция: установка глобального пакета
# ---------------------------------------------------------
function Install-GlobalIfMissing {
param(
[string]$Package,
[string]$Command = ""
)
if ($Command -eq "") { $Command = $Package.Split('/')[-1] }
if (-not (Get-Command $Command -ErrorAction SilentlyContinue)) {
Info "Устанавливаем $Package глобально..."
npm install -g $Package
Success "$Package установлен."
} else {
Success "$Package уже установлен."
}
}
Install-GlobalIfMissing "@microsoft/generator-sharepoint" "yo"
Install-GlobalIfMissing "yo" "yo"
Install-GlobalIfMissing "@rushstack/heft" "heft"
# ---------------------------------------------------------
# 3. Создание package.json
# ---------------------------------------------------------
Info "Создаём package.json..."
$packageJson = @'
{
"name": "spfx-heft-project",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "heft build",
"bundle": "heft build && gulp bundle --ship",
"package-solution": "heft build && gulp package-solution --ship",
"serve": "heft build && gulp serve",
"clean": "heft clean"
},
"overrides": {
"ajv": "^8.0.0"
}
}
'@
$packageJson | Set-Content -Encoding UTF8 "package.json"
Success "package.json создан."
# ---------------------------------------------------------
# 4. Создание структуры папок
# ---------------------------------------------------------
Info "Создаём структуру папок..."
New-Item -ItemType Directory -Force -Path "config" | Out-Null
New-Item -ItemType Directory -Force -Path "src\webparts" | Out-Null
Success "Папки созданы."
# ---------------------------------------------------------
# 5. Запуск Yeoman-генератора SPFx
# ---------------------------------------------------------
Info "Запускаем Yeoman-генератор SPFx..."
Write-Host ""
Write-Host ">>> Сейчас откроется интерактивный генератор Yeoman." -ForegroundColor Yellow
Write-Host ">>> Выберите тип: WebPart, Framework: No framework (или React)." -ForegroundColor Yellow
Write-Host ""
yo "@microsoft/sharepoint"
npm uninstall @microsoft/sp-build-web ajv gulp
npm uninstall @microsoft/rush-stack-compiler-5.3
# ---------------------------------------------------------
# 6. Установка Heft и Rush Stack зависимостей
# ---------------------------------------------------------
Info "Устанавливаем Heft и плагины..."
$devDeps = @(
"@microsoft/spfx-web-build-rig@1.22.1",
"@microsoft/spfx-heft-plugins@1.22.1",
"@microsoft/eslint-config-spfx@1.22.1",
"@microsoft/eslint-plugin-spfx@1.22.17",
"@microsoft/sp-module-interfaces@1.22.1",
"@rushstack/eslint-config@4.5.2",
"@rushstack/heft@1.1.2",
"@types/heft-jest@1.0.2",
"@typescript-eslint/parser@8.46.2"
)
npm install --save-dev @($devDeps) --save-exact --force --legacy-peer-deps
npm install typescript@~5.8.0 --save-dev
Success "Heft и плагины установлены."
# ---------------------------------------------------------
# 7. config/rig.json
# ---------------------------------------------------------
Info "Создаём config\rig.json..."
New-Item -ItemType Directory -Force -Path "config" | Out-Null
$rigJson = @'
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@microsoft/spfx-web-build-rig"
}
'@
$rigJson | Set-Content -Encoding UTF8 "config\rig.json"
Success "config\rig.json создан."
# ---------------------------------------------------------
# 8. config/sass.json
# ---------------------------------------------------------
Info "Создаём config\sass.json..."
New-Item -ItemType Directory -Force -Path "config" | Out-Null
$sassJson = @'
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json",
"extends": "@microsoft/spfx-web-build-rig/profiles/default/config/sass.json"
}
'@
$sassJson | Set-Content -Encoding UTF8 "config\sass.json"
Success "config\sass.json создан."
# ---------------------------------------------------------
# 9. config/typescript.json
# ---------------------------------------------------------
Info "Создаём config\typescript.json..."
New-Item -ItemType Directory -Force -Path "config" | Out-Null
$typescriptJson = @'
{
"extends": "@microsoft/spfx-web-build-rig/profiles/default/config/typescript.json",
"staticAssetsToCopy": {
"fileExtensions": [".resx", ".jpg", ".png", ".woff", ".eot", ".ttf", ".svg", ".gif"],
"includeGlobs": ["webparts/*/loc/*.js"]
}
}
'@
$typescriptJson | Set-Content -Encoding UTF8 "config\typescript.json"
Success "config\typescript.json создан."
# ---------------------------------------------------------
# 8. tsconfig.json (если Yeoman ещё не создал)
# ---------------------------------------------------------
if (-not (Test-Path "tsconfig.json")) {
Info "Создаём tsconfig.json..."
$tsConfig = @'
{
"extends": "./node_modules/@microsoft/spfx-web-build-rig/profiles/default/tsconfig-base.json"
}
'@
$tsConfig | Set-Content -Encoding UTF8 "tsconfig.json"
Success "tsconfig.json создан."
} else {
Success "tsconfig.json уже существует, пропускаем."
}
# ---------------------------------------------------------
# 9. .eslintrc.js (если не создан)
# ---------------------------------------------------------
if (-not (Test-Path ".eslintrc.js")) {
Info "Создаём .eslintrc.js..."
$eslintConfig = @'
require('@rushstack/eslint-config/patch/modern-module-resolution');
module.exports = {
extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'],
parserOptions: { tsconfigRootDir: __dirname }
};
'@
$eslintConfig | Set-Content -Encoding UTF8 ".eslintrc.js"
Success ".eslintrc.js создан."
} else {
Success ".eslintrc.js уже существует, пропускаем."
}
# ---------------------------------------------------------
# 10. .gitignore
# ---------------------------------------------------------
Info "Создаём .gitignore..."
$gitignore = @'
node_modules/
lib/
dist/
temp/
*.sppkg
.heft/
sharepoint/solution/
'@
$gitignore | Set-Content -Encoding UTF8 ".gitignore"
Success ".gitignore создан."
# ---------------------------------------------------------
# 11. Финальный npm install
# ---------------------------------------------------------
$devDeps = @(
"@microsoft/sp-component-base@1.22.1",
"@microsoft/sp-core-library@1.22.1",
"@microsoft/sp-lodash-subset@1.22.1",
"@microsoft/sp-office-ui-fabric-core@1.22.1",
"@microsoft/sp-property-pane@1.22.1",
"@microsoft/sp-webpart-base@1.22.1"
)
Info "Финальный npm install..."
npm install @($devDeps) --save-exact --legacy-peer-deps
Success "Все зависимости установлены."
# ---------------------------------------------------------
# 12. Подключение общей UI-библиотеки @tnsu/ui-kit
# ---------------------------------------------------------
Info "Подключаем @tnsu/ui-kit..."
# .npmrc: адрес registry для скоупа @tnsu (НЕ секрет, можно коммитить).
# Токен на чтение — у каждого разработчика свой, в ~/.npmrc (в проект не кладём).
$npmrc = @'
@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/
'@
$npmrc | Set-Content -Encoding UTF8 ".npmrc"
Success ".npmrc создан (адрес registry @tnsu)."
try {
npm install @tnsu/ui-kit
Success "@tnsu/ui-kit установлен."
Write-Host " Импорт: import { Button } from '@tnsu/ui-kit';" -ForegroundColor DarkGray
Write-Host " Стили: import '@tnsu/ui-kit/styles.css'; (один раз в корневом компоненте)" -ForegroundColor DarkGray
} catch {
Warn "Не удалось поставить @tnsu/ui-kit. Скорее всего нет токена @tnsu в ~/.npmrc."
Warn "Создай токен (Gitea → Настройки → Приложения → package: Read) и пропиши в ~/.npmrc:"
Warn " //git.tnsu.kz/api/packages/timur.gorodetskiy/npm/:_authToken=ТВОЙ_ТОКЕН"
Warn "Потом выполни: npm install @tnsu/ui-kit"
}
# ---------------------------------------------------------
# Готово
# ---------------------------------------------------------
Write-Host ""
Write-Host "=================================================" -ForegroundColor Green
Write-Host " Проект успешно инициализирован!" -ForegroundColor Green
Write-Host "=================================================" -ForegroundColor Green
Write-Host ""
Write-Host " Доступные команды:" -ForegroundColor White
Write-Host ""
Write-Host " npm run serve " -NoNewline -ForegroundColor Cyan
Write-Host "— локальная разработка (gulp serve)"
Write-Host " npm run build " -NoNewline -ForegroundColor Cyan
Write-Host "— сборка через Heft"
Write-Host " npm run bundle " -NoNewline -ForegroundColor Cyan
Write-Host "— бандлинг для продакшна"
Write-Host " npm run package-solution " -NoNewline -ForegroundColor Cyan
Write-Host "— создание .sppkg пакета"
Write-Host " npm run clean " -NoNewline -ForegroundColor Cyan
Write-Host "— очистка артефактов сборки"
Write-Host ""
Write-Host " UI-библиотека: " -NoNewline -ForegroundColor Yellow
Write-Host "@tnsu/ui-kit подключён (см. .npmrc)" -ForegroundColor Cyan
Write-Host ""
Write-Host " Следующий шаг: " -NoNewline -ForegroundColor Yellow
Write-Host "npm run serve" -ForegroundColor Cyan
Write-Host ""
+98
View File
@@ -0,0 +1,98 @@
# Инвентарь поведения — ct / oid / kps → ct-forms-portal
Спецификация для нового объединённого проекта. Источники (РЕФЕРЕНС, не трогаем):
- `ct-create-form` (ctCreateForm) — закупка по позициям из заявок TNSU
- `oid-create-form` (oidCreateForm) — ручной ввод работ (ОИД)
- `kps-ct-form` (kpsCtForm) — эталон организации (decomposed, common/)
Правило: поведение КАЖДОЙ формы переносим точь-в-точь. Общее → shared; различия → per-form.
Заполняется по областям по мере чтения.
---
## Область 1. Машина состояний мастера (навигация по шагам)
**Вывод: логика ОДИНАКОВА во всех трёх → общий хук `useWizard` (в common/), различия — конфигом.**
### Общее (переносим как shared `useWizard`)
Состояние: `currentStep` (1..N), `completedSteps: number[]`, `highestStep`, `formData`.
- `goToStep(step)` → `currentStep = step`; `highestStep = max(highestStep, step)`.
- `goNext()` → `currentStep = min(LAST, s+1)`; `highestStep = max(highestStep, next)`.
- `goPrev()` → `currentStep = max(1, s-1)`. **highestStep НЕ меняет.**
- `markStepComplete(step)` → добавить в completedSteps, если ещё нет (без дублей).
- `ensureHighestStep(step)` → `highestStep = max(highestStep, step)`.
- `updateFormData(partial)` → merge в formData.
- **Кликабельность шага в степпере: `step <= highestStep`** (защита от прыжка вперёд) — уже реализовано в `@tnsu/ui-kit` Stepper.
Это ЧИСТАЯ логика — обязательно покрыть юнит-тестами (clamp 1..LAST, max для highestStep, отсутствие дублей в completed).
### Сохранение состояния (sessionStorage) — общее, ключ per-form
- Персистим `{ formData, currentStep, completedSteps, highestStep }` в sessionStorage.
- Ключ: ct — `ct_form_{ctId}`; kps — свой `sessionKey(ctId)`. → параметр `sessionKeyPrefix` в конфиге формы.
- Пишем, когда известен id сущности (ct.id / ctId). Читаем при инициализации (ленивый init useState).
### Вход по URL + ссылка из уведомления — общее поведение, детали per-form
- id сущности из URL: ct — `ctId`|`ctid`; oid/kps — свои. Признак «ссылка из уведомления» = наличие
ПАРАМЕТРА в нижнем регистре (ct: `ctid`). → параметры `urlIdParam` / `notificationParam` в конфиге.
- Логика приземления при входе по URL:
- **ссылка из уведомления** (согласующий): загрузить из SP → `goToStep(3)` (Шаг 3 главный) +
пометить шаги 1..3 завершёнными (⇒ highestStep=4, доступны все). [ct: markComplete 3 и 4; kps: 1,2,3 — СВЕРИТЬ при переносе, привести к единому].
- **обычный вход по URL** (редактирование): загрузить → приземлиться на «самый дальний
заполненный» шаг (`furthestFilledStep`), пометить завершёнными до него; если статус
approved/rejected → пометить и шаг 4.
- `furthestFilledStep(data)` (kps, эталон): approvers>0 или статус on_approval/approved/rejected → 4;
offers>0 → 3; suppliers>0 → 2; иначе → 1.
### Различия per-form (СОХРАНИТЬ)
- **ct:** хук `useCtForm`; SP-загрузка — большим useEffect в контейнере (~130 строк): восстанавливает
offers, currencyRates, uploadedKpFiles (по сессии + дочитка из SP), `step3DeliveryKm`, suppliers из
офферов. `getCurrentUserProfile → initiatorDepartment`; `getCurrentUserCompany → isRestrictedView`
(company === 'astel-k'). `hidePrices = isRestrictedView || (approvalSent && !userInApprovalChain)`.
Статусы CTStatus: draft/kp_loading/kp_loaded/formed/on_approval/approved/rejected.
- **oid:** хук `useOidForm(context)` (грузит внутри). Шаг 1 — РУЧНОЙ ВВОД работ (Step1WorksInput,
ComboBox) — принципиально другой. `isReadOnly = formData.approvalSent`. Профиль → department+company.
Статусы OIDStatus: draft/formed/on_approval/approved/rejected (нет kp_*). CurrencyRates — inline в Step2.
- **kps (эталон):** хук `useKpsCtForm(ctId)` + отдельный `useSharePointLoad`. Методы `next`/`prev`
(не goNext/goPrev). `replaceFormData`. `deriveCtStatus` — пока заглушка (=formData.status), TODO
реальная логика. `CT_STATUS_META[status] = {label, kind}` (kind → Pill/Badge). `READ_ONLY_STATUSES`.
Единый контракт `StepProps` для всех шагов. Самый чистый — берём за каркас.
### Как ляжет в новый проект
- shared `useWizard(config)` — состояние + переходы + сессия (config: lastStep, sessionKeyPrefix,
urlIdParam, notificationParam).
- per-form: свои шаги, статусы (STATUS_META), SP-загрузка (furthestFilled + восстановление данных),
Step1 (ct/kps — выбор позиций; oid — ручной ввод).
- Степпер — `@tnsu/ui-kit` Stepper; шапка — `@tnsu/ui-kit` FormHeader (kps так уже и делает).
---
## Область 2. Оболочка + реестр (config-driven) — ИСТОЧНИК: ct-forms-list / myDocuments
Готовый каркас для портала. myDocuments — единый дашборд по всем 3 типам, управляется массивом
конфигов DocumentTypeConfig (по одному на CT/OID/KPS в config/*DocumentConfig.ts). Умеет ПРОСМОТР
(реестр+очереди) и СОЗДАНИЕ (кнопка «Создать» → открывает форму-мастер).
### Что уже есть (берём за основу каркаса)
- Сайдбар: пункты из documentTypes (typeLabel + icon), активный тип, бейдж-счётчик «на согласовании
у меня» на пункте. → заменить на @tnsu/ui-kit Sidebar.
- Контент по активному типу: заголовок, тулбар (поиск + фильтр статуса + кнопка «Создать» по
createNewUrl) и 3 вкладки-очереди: «На согласовании у меня» / «Инициализированные мной» /
«Согласованные мной» (Fluent Pivot).
- DocumentTypeConfig (per-type): typeKey, typeLabel, icon, myDocsListName, buildMyDocsFilter,
myDocsSelect, pendingApproval{...}, approvedByMe{...}, buildLinkUrl(row) (ctId для черновика /
ctid иначе), createNewUrl/Label, mapItem(raw→DashboardRow), columns[].
- useDocumentDashboard(context, documentTypes) — грузит myDocs/approvedDocs/pendingDocs.
- StatusBadge (variant neutral/info/success/error) → @tnsu/ui-kit Badge; маппинг статусов — в конфиге типа.
- DocumentTable → @tnsu/ui-kit DataTable (колонки уже описаны в конфиге).
### Fluent → ui-kit при переносе
Pivot → НУЖЕН Tabs (в ките пока НЕТ); SearchBox → TextInput; Dropdown → Select; Spinner/MessageBar →
Alert; Default/PrimaryButton → Button; свой сайдбар → Sidebar; DocumentTable → DataTable.
### Как это меняет каркас портала
Портал = config-driven: массив конфигов типов (CT/OID/KPS), каждый описывает и РЕЕСТР (списки/очереди
из myDocuments), и СОЗДАНИЕ/РЕДАКТИРОВАНИЕ (мастер из ct/oid/kps). Оболочка (Sidebar + вкладки) — из
myDocuments. Мастера — из kps-каркаса. Эталоны: shell/реестр = myDocuments, мастер = kps.
### Пробел ui-kit
Tabs (вкладки-очереди, замена Pivot) — добавить в @tnsu/ui-kit (был в списке: из request-portal CategoryTabs).
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="0" failures="0" errors="0" time="0.003">
</testsuites>
+224
View File
@@ -0,0 +1,224 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* yellow */
.cbranch-no { background: yellow !important; color: #111; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
.highlighted,
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
background: #C21F39 !important;
}
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
/* dark yellow (gold) */
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
.medium .chart { border:1px solid #f9cd0b; }
/* light yellow */
.medium { background: #fff4c2; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.coverage-summary td.empty {
opacity: .5;
padding-top: 4px;
padding-bottom: 4px;
line-height: 1;
color: #888;
}
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}
+87
View File
@@ -0,0 +1,87 @@
/* eslint-disable */
var jumpToCode = (function init() {
// Classes of code we would like to highlight in the file view
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
// Elements to highlight in the file listing view
var fileListingElements = ['td.pct.low'];
// We don't want to select elements that are direct descendants of another match
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
// Selector that finds elements on the page to which we can jump
var selector =
fileListingElements.join(', ') +
', ' +
notSelector +
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
// The NodeList of matching elements
var missingCoverageElements = document.querySelectorAll(selector);
var currentIndex;
function toggleClass(index) {
missingCoverageElements
.item(currentIndex)
.classList.remove('highlighted');
missingCoverageElements.item(index).classList.add('highlighted');
}
function makeCurrent(index) {
toggleClass(index);
currentIndex = index;
missingCoverageElements.item(index).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
});
}
function goToPrevious() {
var nextIndex = 0;
if (typeof currentIndex !== 'number' || currentIndex === 0) {
nextIndex = missingCoverageElements.length - 1;
} else if (missingCoverageElements.length > 1) {
nextIndex = currentIndex - 1;
}
makeCurrent(nextIndex);
}
function goToNext() {
var nextIndex = 0;
if (
typeof currentIndex === 'number' &&
currentIndex < missingCoverageElements.length - 1
) {
nextIndex = currentIndex + 1;
}
makeCurrent(nextIndex);
}
return function jump(event) {
if (
document.getElementById('fileSearch') === document.activeElement &&
document.activeElement != null
) {
// if we're currently focused on the search input, we don't want to navigate
return;
}
switch (event.which) {
case 78: // n
case 74: // j
goToNext();
break;
case 66: // b
case 75: // k
case 80: // p
goToPrevious();
break;
}
};
})();
window.addEventListener('keydown', jumpToCode);
@@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
<coverage lines-valid="0" lines-covered="0" line-rate="NaN" branches-valid="0" branches-covered="0" branch-rate="NaN" timestamp="1786947493328" complexity="0" version="0.1">
<sources>
<source>C:\Users\gorodetskiy.timur\Documents\Projects\ct-forms-portal</source>
</sources>
<packages>
</packages>
</coverage>
@@ -0,0 +1,2 @@
{"total": {"lines":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"},"statements":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"},"functions":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"},"branches":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"}}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

+101
View File
@@ -0,0 +1,101 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>All files</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Statements</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Lines</span>
<span class='fraction'>0/0</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-08-17T06:18:13.341Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

+210
View File
@@ -0,0 +1,210 @@
/* eslint-disable */
var addSorting = (function() {
'use strict';
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() {
return document.querySelector('.coverage-summary');
}
// returns the thead element of the summary table
function getTableHeader() {
return getTable().querySelector('thead tr');
}
// returns the tbody element of the summary table
function getTableBody() {
return getTable().querySelector('tbody');
}
// returns the th element for nth column
function getNthColumn(n) {
return getTableHeader().querySelectorAll('th')[n];
}
function onFilterInput() {
const searchValue = document.getElementById('fileSearch').value;
const rows = document.getElementsByTagName('tbody')[0].children;
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
// it will be treated as a plain text search
let searchRegex;
try {
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
} catch (error) {
searchRegex = null;
}
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
let isMatch = false;
if (searchRegex) {
// If a valid regex was created, use it for matching
isMatch = searchRegex.test(row.textContent);
} else {
// Otherwise, fall back to the original plain text search
isMatch = row.textContent
.toLowerCase()
.includes(searchValue.toLowerCase());
}
row.style.display = isMatch ? '' : 'none';
}
}
// loads the search box
function addSearchBox() {
var template = document.getElementById('filterTemplate');
var templateClone = template.content.cloneNode(true);
templateClone.getElementById('fileSearch').oninput = onFilterInput;
template.parentElement.appendChild(templateClone);
}
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function(a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function(a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc
? ' sorted-desc'
: ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function() {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i = 0; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function() {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData();
addSearchBox();
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);
@@ -0,0 +1,107 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var React = tslib_1.__importStar(require("react"));
var ReactDom = tslib_1.__importStar(require("react-dom"));
var sp_core_library_1 = require("@microsoft/sp-core-library");
var sp_property_pane_1 = require("@microsoft/sp-property-pane");
var sp_webpart_base_1 = require("@microsoft/sp-webpart-base");
var strings = tslib_1.__importStar(require("CtFormsPortalWebPartStrings"));
var CtFormsPortal_1 = tslib_1.__importDefault(require("./components/CtFormsPortal"));
var CtFormsPortalWebPart = /** @class */ (function (_super) {
tslib_1.__extends(CtFormsPortalWebPart, _super);
function CtFormsPortalWebPart() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this._isDarkTheme = false;
_this._environmentMessage = '';
return _this;
}
CtFormsPortalWebPart.prototype.render = function () {
var element = React.createElement(CtFormsPortal_1.default, {
description: this.properties.description,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName,
context: this.context
});
ReactDom.render(element, this.domElement);
};
CtFormsPortalWebPart.prototype.onInit = function () {
var _this = this;
return this._getEnvironmentMessage().then(function (message) {
_this._environmentMessage = message;
});
};
CtFormsPortalWebPart.prototype._getEnvironmentMessage = function () {
var _this = this;
if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
.then(function (context) {
var environmentMessage = '';
switch (context.app.host.name) {
case 'Office': // running in Office
environmentMessage = _this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;
break;
case 'Outlook': // running in Outlook
environmentMessage = _this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;
break;
case 'Teams': // running in Teams
case 'TeamsModern':
environmentMessage = _this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
break;
default:
environmentMessage = strings.UnknownEnvironment;
}
return environmentMessage;
});
}
return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);
};
CtFormsPortalWebPart.prototype.onThemeChanged = function (currentTheme) {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
var semanticColors = currentTheme.semanticColors;
if (semanticColors) {
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null);
this.domElement.style.setProperty('--link', semanticColors.link || null);
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null);
}
};
CtFormsPortalWebPart.prototype.onDispose = function () {
ReactDom.unmountComponentAtNode(this.domElement);
};
Object.defineProperty(CtFormsPortalWebPart.prototype, "dataVersion", {
get: function () {
return sp_core_library_1.Version.parse('1.0');
},
enumerable: false,
configurable: true
});
CtFormsPortalWebPart.prototype.getPropertyPaneConfiguration = function () {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
(0, sp_property_pane_1.PropertyPaneTextField)('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
};
return CtFormsPortalWebPart;
}(sp_webpart_base_1.BaseClientSideWebPart));
exports.default = CtFormsPortalWebPart;
//# sourceMappingURL=CtFormsPortalWebPart.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "459579c7-954c-469e-b707-ca75d808603f",
"alias": "CtFormsPortalWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"],
"supportsThemeVariants": true,
"supportsFullBleed": true,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced
"group": { "default": "Advanced" },
"title": { "default": "ct-forms-portal" },
"description": { "default": "ct-forms-portal description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "ct-forms-portal"
}
}]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var React = tslib_1.__importStar(require("react"));
require("@tnsu/ui-kit/styles.css");
var ui_kit_1 = require("@tnsu/ui-kit");
var CtFormsPortal_module_scss_1 = tslib_1.__importDefault(require("./CtFormsPortal.module.scss"));
var Dashboard_1 = require("./Dashboard");
// Корень веб-парта: спрайт иконок (один раз) + дашборд-реестр.
// Импорт CtFormsPortal.module.scss обязателен — в нём :global full-width правила канвы.
var CtFormsPortal = function (props) { return (React.createElement("div", { className: CtFormsPortal_module_scss_1.default.root },
React.createElement(ui_kit_1.IconSprite, null),
React.createElement(Dashboard_1.Dashboard, { context: props.context }))); };
exports.default = CtFormsPortal;
//# sourceMappingURL=CtFormsPortal.js.map
@@ -0,0 +1 @@
{"version":3,"file":"CtFormsPortal.js","sourceRoot":"","sources":["../../../../src/webparts/ctFormsPortal/components/CtFormsPortal.tsx"],"names":[],"mappings":";;;AAAA,mDAA+B;AAC/B,mCAAiC;AACjC,uCAA0C;AAC1C,kGAAiD;AAEjD,yCAAwC;AAExC,+DAA+D;AAC/D,wFAAwF;AACxF,IAAM,aAAa,GAAkC,UAAC,KAAK,IAAK,OAAA,CAC9D,6BAAK,SAAS,EAAE,mCAAM,CAAC,IAAI;IACzB,oBAAC,mBAAU,OAAG;IACd,oBAAC,qBAAS,IAAC,OAAO,EAAE,KAAK,CAAC,OAAO,GAAI,CACjC,CACP,EAL+D,CAK/D,CAAC;AAEF,kBAAe,aAAa,CAAC","sourcesContent":["import * as React from 'react';\nimport '@tnsu/ui-kit/styles.css';\nimport { IconSprite } from '@tnsu/ui-kit';\nimport styles from './CtFormsPortal.module.scss';\nimport type { ICtFormsPortalProps } from './ICtFormsPortalProps';\nimport { Dashboard } from './Dashboard';\n\n// Корень веб-парта: спрайт иконок (один раз) + дашборд-реестр.\n// Импорт CtFormsPortal.module.scss обязателен — в нём :global full-width правила канвы.\nconst CtFormsPortal: React.FC<ICtFormsPortalProps> = (props) => (\n <div className={styles.root}>\n <IconSprite />\n <Dashboard context={props.context} />\n </div>\n);\n\nexport default CtFormsPortal;\n"]}
@@ -0,0 +1,11 @@
:global #workbenchPageContent,
:global #spPageCanvasContent,
:global .SPCanvas-canvas,
:global .CanvasComponent.LCS .CanvasZone,
:global .CanvasZone {
max-width: 100% !important;
}
.root {
width: 100%;
}
@@ -0,0 +1,106 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Dashboard = void 0;
var tslib_1 = require("tslib");
var React = tslib_1.__importStar(require("react"));
var react_1 = require("react");
var Dashboard_module_scss_1 = tslib_1.__importDefault(require("./Dashboard.module.scss"));
var ui_kit_1 = require("@tnsu/ui-kit");
var config_1 = require("./config");
var useDocumentDashboard_1 = require("./registry/useDocumentDashboard");
// Иконка сайдбара по типу (kit-иконки; Fluent-иконки из конфигов тут не используем).
var TYPE_ICON = { ct: 'clipboard', oid: 'chart', kps: 'building' };
// Цвет статуса DashboardRow → вариант нашего Badge (error → danger).
var VARIANT_MAP = {
neutral: 'neutral',
info: 'info',
success: 'success',
error: 'danger'
};
function formatDate(iso) {
if (!iso)
return '';
var d = new Date(iso);
var dd = d.getDate() < 10 ? "0".concat(d.getDate()) : "".concat(d.getDate());
var mm = d.getMonth() + 1 < 10 ? "0".concat(d.getMonth() + 1) : "".concat(d.getMonth() + 1);
return "".concat(dd, ".").concat(mm, ".").concat(d.getFullYear());
}
// Дашборд-реестр (config-driven, данные из SharePoint). Оболочка и элементы — из @tnsu/ui-kit.
var Dashboard = function (_a) {
var _b;
var context = _a.context;
var _c = (0, useDocumentDashboard_1.useDocumentDashboard)(context, config_1.DOCUMENT_TYPES), myDocs = _c.myDocs, approvedDocs = _c.approvedDocs, pendingDocs = _c.pendingDocs, isLoading = _c.isLoading, error = _c.error;
var _d = (0, react_1.useState)(config_1.DOCUMENT_TYPES[0].typeKey), activeType = _d[0], setActiveType = _d[1];
var _e = (0, react_1.useState)('pending'), queue = _e[0], setQueue = _e[1];
var _f = (0, react_1.useState)(false), collapsed = _f[0], setCollapsed = _f[1];
var _g = (0, react_1.useState)(false), mobileOpen = _g[0], setMobileOpen = _g[1];
var _h = (0, react_1.useState)(''), search = _h[0], setSearch = _h[1];
var _j = (0, react_1.useState)(''), status = _j[0], setStatus = _j[1];
var cfg = config_1.DOCUMENT_TYPES.filter(function (t) { return t.typeKey === activeType; })[0];
var byType = function (arr) { return arr.filter(function (r) { return r.typeKey === activeType; }); };
var pendingForType = byType(pendingDocs);
var mineForType = byType(myDocs);
var approvedForType = byType(approvedDocs);
var sidebarItems = config_1.DOCUMENT_TYPES.map(function (t) {
var _a;
return ({
id: t.typeKey,
title: t.typeLabel,
icon: (_a = TYPE_ICON[t.typeKey]) !== null && _a !== void 0 ? _a : 'document',
count: pendingDocs.filter(function (r) { return r.typeKey === t.typeKey; }).length
});
});
var tabs = [
{ id: 'pending', label: 'На согласовании у меня', count: pendingForType.length },
{ id: 'mine', label: 'Инициализированные мной', count: mineForType.length },
{ id: 'approved', label: 'Согласованные мной', count: approvedForType.length }
];
var activeRows = queue === 'pending' ? pendingForType : queue === 'mine' ? mineForType : approvedForType;
var statusOptions = Array.from(new Set(activeRows.map(function (r) { return r.statusLabel; }))).map(function (s) { return ({
value: s,
label: s
}); });
var rows = activeRows
.filter(function (r) { var _a; return !search || "".concat(r.title, " ").concat((_a = r.subtitle) !== null && _a !== void 0 ? _a : '').toLowerCase().indexOf(search.toLowerCase()) !== -1; })
.filter(function (r) { return !status || r.statusLabel === status; });
var renderCell = function (row, key) {
var _a;
if (key === 'status') {
return (React.createElement(ui_kit_1.Badge, { variant: VARIANT_MAP[row.statusVariant], size: "sm" }, row.statusLabel));
}
if (key === 'created')
return formatDate(row.createdDate);
return String((_a = row[key]) !== null && _a !== void 0 ? _a : '');
};
var columns = cfg.columns.map(function (c) { return ({
key: c.key,
title: c.label,
width: c.minWidth ? "".concat(c.minWidth, "px") : undefined,
render: function (row) { return renderCell(row, c.key); }
}); });
var selectType = function (id) {
setActiveType(String(id));
setQueue('pending');
setSearch('');
setStatus('');
};
return (React.createElement(ui_kit_1.AppLayout, { sidebar: React.createElement(ui_kit_1.Sidebar, { items: sidebarItems, activeId: activeType, onChange: selectType, brand: "\u0414\u043E\u0433\u043E\u0432\u043E\u0440\u0430", collapsed: collapsed, onToggleCollapsed: function () { return setCollapsed(function (c) { return !c; }); }, mobileOpen: mobileOpen, onMobileClose: function () { return setMobileOpen(false); } }), mobileToggle: React.createElement(ui_kit_1.SidebarToggle, { onClick: function () { return setMobileOpen(true); } }) },
React.createElement("div", { className: Dashboard_module_scss_1.default.head },
React.createElement("h2", { className: Dashboard_module_scss_1.default.title }, cfg.typeLabel),
cfg.createNewUrl && (React.createElement(ui_kit_1.Button, { variant: "accent", onClick: function () { return window.open(cfg.createNewUrl, '_blank'); } }, (_b = cfg.createNewLabel) !== null && _b !== void 0 ? _b : 'Создать'))),
React.createElement("div", { className: Dashboard_module_scss_1.default.toolbar },
React.createElement("div", { className: Dashboard_module_scss_1.default.search },
React.createElement(ui_kit_1.TextInput, { value: search, onChange: setSearch, placeholder: "\u041F\u043E\u0438\u0441\u043A \u043F\u043E \u043D\u043E\u043C\u0435\u0440\u0443 \u0438\u043B\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0443" })),
React.createElement("div", { className: Dashboard_module_scss_1.default.filter },
React.createElement(ui_kit_1.Select, { value: status, onChange: setStatus, options: statusOptions, placeholder: "\u0412\u0441\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u044B" }))),
React.createElement(ui_kit_1.Tabs, { tabs: tabs, activeId: queue, onChange: function (id) { return setQueue(id); } }),
error && (React.createElement("div", { className: Dashboard_module_scss_1.default.state },
React.createElement(ui_kit_1.Alert, { variant: "err" },
"\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435: ",
error))),
!error && isLoading && React.createElement("div", { className: Dashboard_module_scss_1.default.state }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026"),
!error && !isLoading && (React.createElement("div", { className: Dashboard_module_scss_1.default.tableWrap },
React.createElement(ui_kit_1.DataTable, { columns: columns, rows: rows, rowKey: function (r) { return "".concat(r.typeKey, "-").concat(r.id); }, onRowClick: function (r) { return window.open(cfg.buildLinkUrl(r), '_blank'); }, emptyText: "\u041D\u0435\u0442 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432" })))));
};
exports.Dashboard = Dashboard;
//# sourceMappingURL=Dashboard.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
.head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.title {
margin: 0;
font-family: "Lora", Georgia, serif;
font-size: 22px;
font-weight: 600;
color: #0f2440;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search {
width: 280px;
max-width: 100%;
}
.filter {
width: 200px;
}
.tableWrap {
margin-top: 16px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
box-shadow: 0 1px 2px rgba(15, 36, 64, 0.04), 0 1px 3px rgba(15, 36, 64, 0.06);
overflow: hidden;
}
.state {
margin-top: 16px;
color: #6b7280;
font-size: 13px;
}
@media (max-width: 640px) {
.search,
.filter {
width: 100%;
}
}
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=ICtFormsPortalProps.js.map
@@ -0,0 +1 @@
{"version":3,"file":"ICtFormsPortalProps.js","sourceRoot":"","sources":["../../../../src/webparts/ctFormsPortal/components/ICtFormsPortalProps.ts"],"names":[],"mappings":"","sourcesContent":["import { WebPartContext } from '@microsoft/sp-webpart-base';\n\nexport interface ICtFormsPortalProps {\n description: string;\n isDarkTheme: boolean;\n environmentMessage: string;\n hasTeamsContext: boolean;\n userDisplayName: string;\n context: WebPartContext;\n}\n"]}
@@ -0,0 +1,79 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCtDocumentConfig = createCtDocumentConfig;
var STATUS_LABEL = {
'Черновик': 'Сформирована',
'На согласовании': 'На согласовании',
'Согласовано': 'Согласована',
'Отклонено': 'Отклонена'
};
var STATUS_VARIANT = {
'Черновик': 'neutral',
'На согласовании': 'info',
'Согласовано': 'success',
'Отклонено': 'error'
};
function createCtDocumentConfig() {
return {
typeKey: 'ct',
typeLabel: 'СТ ОМТС',
icon: 'ShoppingCart',
// Списки СТ ОМТС — на сайте СЭД. Явно, чтобы портал не зависел от места размещения.
siteUrl: 'https://tnsukz.sharepoint.com/sites/sed',
myDocsListName: 'Сравнительная таблица',
buildMyDocsFilter: function (userId) { return "AuthorId eq ".concat(userId); },
myDocsSelect: 'Id,Title,number,project,status,Created',
pendingApproval: {
approvalListName: 'Согласование СТ',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u041D\u0430 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u0438\u0438'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
approvedByMe: {
approvalListName: 'Согласование СТ',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u0421\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043E'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
buildLinkUrl: function (row) {
var base = 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица.aspx';
var param = row.status === 'Черновик' ? 'ctId' : 'ctid';
return "".concat(base, "?").concat(param, "=").concat(row.id);
},
createNewUrl: 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица.aspx',
createNewLabel: 'Создать новую форму СТ',
mapItem: function (raw) {
var _a, _b, _c, _d, _e;
var spStatus = String((_a = raw['status']) !== null && _a !== void 0 ? _a : '');
return {
typeKey: 'ct',
id: String(raw['Id']),
title: "\u0421\u0422 \u2116".concat((_b = raw['number']) !== null && _b !== void 0 ? _b : raw['Title']),
subtitle: String((_c = raw['project']) !== null && _c !== void 0 ? _c : ''),
status: spStatus,
statusLabel: (_d = STATUS_LABEL[spStatus]) !== null && _d !== void 0 ? _d : spStatus,
statusVariant: (_e = STATUS_VARIANT[spStatus]) !== null && _e !== void 0 ? _e : 'neutral',
createdDate: raw['Created'] ? String(raw['Created']) : undefined
};
},
columns: [
{ key: 'title', label: 'Номер СТ', minWidth: 120 },
{ key: 'subtitle', label: 'Проект', minWidth: 180 },
{ key: 'status', label: 'Статус', minWidth: 130 },
{ key: 'created', label: 'Создана', minWidth: 110 }
]
};
}
//# sourceMappingURL=ctDocumentConfig.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DOCUMENT_TYPES = void 0;
var ctDocumentConfig_1 = require("./ctDocumentConfig");
var oidDocumentConfig_1 = require("./oidDocumentConfig");
var kpsDocumentConfig_1 = require("./kpsDocumentConfig");
// Все типы документов портала. Добавить тип = добавить конфиг сюда.
exports.DOCUMENT_TYPES = [
(0, ctDocumentConfig_1.createCtDocumentConfig)(),
(0, oidDocumentConfig_1.createOidDocumentConfig)(),
(0, kpsDocumentConfig_1.createKpsDocumentConfig)()
];
//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/webparts/ctFormsPortal/components/config/index.ts"],"names":[],"mappings":";;;AACA,uDAA4D;AAC5D,yDAA8D;AAC9D,yDAA8D;AAE9D,oEAAoE;AACvD,QAAA,cAAc,GAAyB;IAClD,IAAA,yCAAsB,GAAE;IACxB,IAAA,2CAAuB,GAAE;IACzB,IAAA,2CAAuB,GAAE;CAC1B,CAAC","sourcesContent":["import { DocumentTypeConfig } from '../registry/documentRegistry';\nimport { createCtDocumentConfig } from './ctDocumentConfig';\nimport { createOidDocumentConfig } from './oidDocumentConfig';\nimport { createKpsDocumentConfig } from './kpsDocumentConfig';\n\n// Все типы документов портала. Добавить тип = добавить конфиг сюда.\nexport const DOCUMENT_TYPES: DocumentTypeConfig[] = [\n createCtDocumentConfig(),\n createOidDocumentConfig(),\n createKpsDocumentConfig()\n];\n"]}
@@ -0,0 +1,80 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createKpsDocumentConfig = createKpsDocumentConfig;
var KPS_SITE_URL = 'https://tnsukz.sharepoint.com/sites/kps';
var KPS_FORM_PAGE = "".concat(KPS_SITE_URL, "/SitePages/\u0421\u0440\u0430\u0432\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u0430\u044F-\u0442\u0430\u0431\u043B\u0438\u0446\u0430.aspx");
var STATUS_LABEL = {
'Черновик': 'Сформирована',
'На согласовании': 'На согласовании',
'Согласовано': 'Согласована',
'Отклонено': 'Отклонена'
};
var STATUS_VARIANT = {
'Черновик': 'neutral',
'На согласовании': 'info',
'Согласовано': 'success',
'Отклонено': 'error'
};
function createKpsDocumentConfig() {
return {
typeKey: 'kps',
typeLabel: 'СТ КПС',
icon: 'Processing',
// Списки КПС — на отдельном сайт-коллекшене /sites/kps.
siteUrl: KPS_SITE_URL,
myDocsListName: 'Сравнительная таблица',
buildMyDocsFilter: function (userId) { return "AuthorId eq ".concat(userId); },
myDocsSelect: 'Id,Title,number,project,status,Created',
pendingApproval: {
approvalListName: 'Согласование СТ',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u041D\u0430 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u0438\u0438'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
approvedByMe: {
approvalListName: 'Согласование СТ',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u0421\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043E'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
buildLinkUrl: function (row) {
var param = row.status === 'Черновик' ? 'ctId' : 'ctid';
return "".concat(KPS_FORM_PAGE, "?").concat(param, "=").concat(row.id);
},
createNewUrl: KPS_FORM_PAGE,
createNewLabel: 'Создать новую форму КПС',
mapItem: function (raw) {
var _a, _b, _c, _d, _e;
var spStatus = String((_a = raw['status']) !== null && _a !== void 0 ? _a : '');
return {
typeKey: 'kps',
id: String(raw['Id']),
title: "\u0421\u0422 \u2116".concat((_b = raw['number']) !== null && _b !== void 0 ? _b : raw['Title']),
subtitle: String((_c = raw['project']) !== null && _c !== void 0 ? _c : ''),
status: spStatus,
statusLabel: (_d = STATUS_LABEL[spStatus]) !== null && _d !== void 0 ? _d : spStatus,
statusVariant: (_e = STATUS_VARIANT[spStatus]) !== null && _e !== void 0 ? _e : 'neutral',
createdDate: raw['Created'] ? String(raw['Created']) : undefined
};
},
columns: [
{ key: 'title', label: 'Номер СТ', minWidth: 120 },
{ key: 'subtitle', label: 'Проект', minWidth: 180 },
{ key: 'status', label: 'Статус', minWidth: 130 },
{ key: 'created', label: 'Создана', minWidth: 110 }
]
};
}
//# sourceMappingURL=kpsDocumentConfig.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,21 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MOCK_ROWS = exports.FORM_TYPES = void 0;
// Типы форм (позже иконки заменим на профильные, когда добавим их в ui-kit).
exports.FORM_TYPES = [
{ typeKey: 'ct', typeLabel: 'СТ ОМТС', icon: 'clipboard', createLabel: 'Создать новую форму СТ' },
{ typeKey: 'oid', typeLabel: 'СТ ОИД', icon: 'chart', createLabel: 'Создать новую форму ОИД' },
{ typeKey: 'kps', typeLabel: 'СТ КПС', icon: 'building', createLabel: 'Создать новую форму КПС' }
];
// Моки реестра (пока без SharePoint) — чтобы увидеть каркас.
exports.MOCK_ROWS = [
{ id: '53', typeKey: 'ct', queue: 'pending', number: 'СТ №53', project: 'Строительство 5-этажного жилого дома в п.Топар', status: 'На согласовании', statusVariant: 'info', created: '04.06.2026' },
{ id: '52', typeKey: 'ct', queue: 'mine', number: 'СТ №52', project: 'Офис TANSU', status: 'Сформирована', statusVariant: 'neutral', created: '02.06.2026' },
{ id: '48', typeKey: 'ct', queue: 'mine', number: 'СТ №48', project: 'Склад в Караганде', status: 'На согласовании', statusVariant: 'info', created: '28.05.2026' },
{ id: '40', typeKey: 'ct', queue: 'approved', number: 'СТ №40', project: 'Строительство 5-этажного жилого дома в п.Топар', status: 'Согласована', statusVariant: 'success', created: '20.05.2026' },
{ id: '21', typeKey: 'oid', queue: 'mine', number: 'ОИД №21', project: 'Реконструкция кровли', status: 'Сформирована', statusVariant: 'neutral', created: '01.06.2026' },
{ id: '19', typeKey: 'oid', queue: 'approved', number: 'ОИД №19', project: 'Благоустройство территории', status: 'Согласована', statusVariant: 'success', created: '15.05.2026' },
{ id: '17', typeKey: 'oid', queue: 'mine', number: 'ОИД №17', project: 'Отделочные работы', status: 'Отклонена', statusVariant: 'danger', created: '10.05.2026' },
{ id: '9', typeKey: 'kps', queue: 'mine', number: 'КПС №9', project: 'Поставка материалов', status: 'На согласовании', statusVariant: 'info', created: '03.06.2026' }
];
//# sourceMappingURL=mock.js.map
@@ -0,0 +1 @@
{"version":3,"file":"mock.js","sourceRoot":"","sources":["../../../../../src/webparts/ctFormsPortal/components/config/mock.ts"],"names":[],"mappings":";;;AAEA,6EAA6E;AAChE,QAAA,UAAU,GAAsB;IAC3C,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IACjG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAE;IAC9F,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,yBAAyB,EAAE;CAClG,CAAC;AAEF,6DAA6D;AAChD,QAAA,SAAS,GAAmB;IACvC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,gDAAgD,EAAE,MAAM,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE;IACnM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE;IAC5J,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE;IACnK,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,gDAAgD,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE;IAEnM,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE;IACxK,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,4BAA4B,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE;IACjL,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE;IAEjK,EAAE,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE;CACtK,CAAC","sourcesContent":["import { IFormTypeConfig, IRegistryRow } from './types';\n\n// Типы форм (позже иконки заменим на профильные, когда добавим их в ui-kit).\nexport const FORM_TYPES: IFormTypeConfig[] = [\n { typeKey: 'ct', typeLabel: 'СТ ОМТС', icon: 'clipboard', createLabel: 'Создать новую форму СТ' },\n { typeKey: 'oid', typeLabel: 'СТ ОИД', icon: 'chart', createLabel: 'Создать новую форму ОИД' },\n { typeKey: 'kps', typeLabel: 'СТ КПС', icon: 'building', createLabel: 'Создать новую форму КПС' }\n];\n\n// Моки реестра (пока без SharePoint) — чтобы увидеть каркас.\nexport const MOCK_ROWS: IRegistryRow[] = [\n { id: '53', typeKey: 'ct', queue: 'pending', number: 'СТ №53', project: 'Строительство 5-этажного жилого дома в п.Топар', status: 'На согласовании', statusVariant: 'info', created: '04.06.2026' },\n { id: '52', typeKey: 'ct', queue: 'mine', number: 'СТ №52', project: 'Офис TANSU', status: 'Сформирована', statusVariant: 'neutral', created: '02.06.2026' },\n { id: '48', typeKey: 'ct', queue: 'mine', number: 'СТ №48', project: 'Склад в Караганде', status: 'На согласовании', statusVariant: 'info', created: '28.05.2026' },\n { id: '40', typeKey: 'ct', queue: 'approved', number: 'СТ №40', project: 'Строительство 5-этажного жилого дома в п.Топар', status: 'Согласована', statusVariant: 'success', created: '20.05.2026' },\n\n { id: '21', typeKey: 'oid', queue: 'mine', number: 'ОИД №21', project: 'Реконструкция кровли', status: 'Сформирована', statusVariant: 'neutral', created: '01.06.2026' },\n { id: '19', typeKey: 'oid', queue: 'approved', number: 'ОИД №19', project: 'Благоустройство территории', status: 'Согласована', statusVariant: 'success', created: '15.05.2026' },\n { id: '17', typeKey: 'oid', queue: 'mine', number: 'ОИД №17', project: 'Отделочные работы', status: 'Отклонена', statusVariant: 'danger', created: '10.05.2026' },\n\n { id: '9', typeKey: 'kps', queue: 'mine', number: 'КПС №9', project: 'Поставка материалов', status: 'На согласовании', statusVariant: 'info', created: '03.06.2026' }\n];\n"]}
@@ -0,0 +1,85 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createOidDocumentConfig = createOidDocumentConfig;
var STATUS_LABEL = {
'Черновик': 'Сформирована',
'formed': 'Сформирована',
'На согласовании': 'На согласовании',
'on_approval': 'На согласовании',
'Согласовано': 'Согласована',
'approved': 'Согласована',
'Отклонено': 'Отклонена'
};
var STATUS_VARIANT = {
'Черновик': 'neutral',
'formed': 'neutral',
'На согласовании': 'info',
'on_approval': 'info',
'Согласовано': 'success',
'approved': 'success',
'Отклонено': 'error'
};
function createOidDocumentConfig() {
return {
typeKey: 'oid',
typeLabel: 'СТ ОИД',
icon: 'CRMServices',
// Списки СТ ОИД — на сайте СЭД. Явно, чтобы портал не зависел от места размещения.
siteUrl: 'https://tnsukz.sharepoint.com/sites/sed',
myDocsListName: 'Сравнительная таблица ОИД',
buildMyDocsFilter: function (userId) { return "AuthorId eq ".concat(userId); },
myDocsSelect: 'Id,Title,number,project,status,Created',
pendingApproval: {
approvalListName: 'Согласование СТ ОИД',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u041D\u0430 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u0438\u0438'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица ОИД',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
approvedByMe: {
approvalListName: 'Согласование СТ ОИД',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u0421\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043E'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица ОИД',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
buildLinkUrl: function (row) {
var base = 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица-ОИД.aspx';
var param = row.status === 'Черновик' || row.status === 'formed' ? 'oidId' : 'oidid';
return "".concat(base, "?").concat(param, "=").concat(row.id);
},
createNewUrl: 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица-ОИД.aspx',
createNewLabel: 'Создать новую форму ОИД',
mapItem: function (raw) {
var _a, _b, _c, _d, _e;
var spStatus = String((_a = raw['status']) !== null && _a !== void 0 ? _a : '');
return {
typeKey: 'oid',
id: String(raw['Id']),
title: "\u0421\u0422 \u2116".concat((_b = raw['number']) !== null && _b !== void 0 ? _b : raw['Title']),
subtitle: String((_c = raw['project']) !== null && _c !== void 0 ? _c : ''),
status: spStatus,
statusLabel: (_d = STATUS_LABEL[spStatus]) !== null && _d !== void 0 ? _d : spStatus,
statusVariant: (_e = STATUS_VARIANT[spStatus]) !== null && _e !== void 0 ? _e : 'neutral',
createdDate: raw['Created'] ? String(raw['Created']) : undefined
};
},
columns: [
{ key: 'title', label: 'Номер СТ', minWidth: 120 },
{ key: 'subtitle', label: 'Проект', minWidth: 180 },
{ key: 'status', label: 'Статус', minWidth: 130 },
{ key: 'created', label: 'Создана', minWidth: 110 }
]
};
}
//# sourceMappingURL=oidDocumentConfig.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../src/webparts/ctFormsPortal/components/config/types.ts"],"names":[],"mappings":"","sourcesContent":["import { IconName, BadgeVariant } from '@tnsu/ui-kit';\n\n// Очередь реестра (вкладки).\nexport type QueueKey = 'pending' | 'mine' | 'approved';\n\n// Конфиг типа формы (CT/OID/KPS). Пока — минимум для оболочки/реестра.\n// Позже расширим: SP-списки, фильтры, buildLinkUrl, мастер и т.д.\nexport interface IFormTypeConfig {\n typeKey: string;\n typeLabel: string;\n icon: IconName;\n createLabel: string;\n}\n\n// Унифицированная строка реестра. statusVariant — цвет бейджа (маппинг в конфиге типа).\nexport interface IRegistryRow {\n id: string;\n typeKey: string;\n queue: QueueKey;\n number: string;\n project: string;\n status: string;\n statusVariant: BadgeVariant;\n created: string;\n}\n"]}
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=documentRegistry.js.map
@@ -0,0 +1 @@
{"version":3,"file":"documentRegistry.js","sourceRoot":"","sources":["../../../../../src/webparts/ctFormsPortal/components/registry/documentRegistry.ts"],"names":[],"mappings":"","sourcesContent":["import { WebPartContext } from '@microsoft/sp-webpart-base';\n\n// Унифицированная строка реестра (перенесено из ct-forms-list/myDocuments).\nexport interface DashboardRow {\n typeKey: string;\n id: string;\n title: string;\n subtitle?: string;\n status: string;\n statusLabel: string;\n statusVariant: 'neutral' | 'info' | 'success' | 'error';\n createdDate?: string;\n}\n\nexport interface ColumnDef {\n key: string;\n label: string;\n minWidth?: number;\n}\n\nexport interface ApprovalSectionConfig {\n approvalListName: string;\n buildPendingFilter: (userId: number) => string;\n approvalSelect: string;\n approvalExpand?: string;\n /** поле в записи согласования, хранящее ID родителя */\n parentIdField: string;\n buildParentQuery: (parentIds: number[]) => { listName: string; filter: string; select: string };\n}\n\n// Конфиг типа документа (CT/OID/KPS): где лежат SP-списки, как фильтровать,\n// как маппить в строку и как открыть форму. Различия типов живут ЗДЕСЬ.\nexport interface DocumentTypeConfig {\n typeKey: string;\n typeLabel: string;\n /** Имя иконки Fluent (легаси из myDocuments); в портале сайдбар берёт kit-иконку по typeKey. */\n icon?: string;\n /** Абсолютный URL сайта со списками этого типа. Не задан — текущий сайт. */\n siteUrl?: string;\n myDocsListName: string;\n buildMyDocsFilter: (userId: number) => string;\n myDocsSelect: string;\n myDocsExpand?: string;\n /** null = нет согласования у этого типа */\n pendingApproval: ApprovalSectionConfig | null;\n /** null = нет вкладки \"Согласованные мной\" */\n approvedByMe: ApprovalSectionConfig | null;\n mapItem: (raw: Record<string, unknown>, context: WebPartContext) => DashboardRow;\n buildLinkUrl: (row: DashboardRow) => string;\n createNewUrl?: string;\n createNewLabel?: string;\n columns: ColumnDef[];\n}\n"]}
@@ -0,0 +1,175 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DocumentsService = void 0;
var tslib_1 = require("tslib");
var sp_http_1 = require("@microsoft/sp-http");
// Загрузка реестра из SharePoint (перенесено из ct-forms-list/myDocuments).
// Поддерживает типы на разных сайт-коллекшенах (KPS на /sites/kps).
var DocumentsService = /** @class */ (function () {
function DocumentsService(context) {
this.context = context;
this.userIdCache = new Map();
}
Object.defineProperty(DocumentsService.prototype, "currentUserId", {
get: function () {
var _a;
return Number((_a = this.context.pageContext.legacyPageContext['userId']) !== null && _a !== void 0 ? _a : 0);
},
enumerable: false,
configurable: true
});
Object.defineProperty(DocumentsService.prototype, "webUrl", {
get: function () {
return this.context.pageContext.web.absoluteUrl;
},
enumerable: false,
configurable: true
});
DocumentsService.prototype.siteUrlFor = function (config) {
var _a;
return (_a = config.siteUrl) !== null && _a !== void 0 ? _a : this.webUrl;
};
DocumentsService.prototype.resolveUserId = function (config) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var site, cached, response, json;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
site = this.siteUrlFor(config);
if (site === this.webUrl)
return [2 /*return*/, this.currentUserId];
cached = this.userIdCache.get(site);
if (cached !== undefined)
return [2 /*return*/, cached];
return [4 /*yield*/, this.context.spHttpClient.get("".concat(site, "/_api/web/currentUser?$select=Id"), sp_http_1.SPHttpClient.configurations.v1)];
case 1:
response = _a.sent();
if (!response.ok) {
throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u043D\u0430 \u0441\u0430\u0439\u0442\u0435 \"".concat(site, "\": ").concat(response.status));
}
return [4 /*yield*/, response.json()];
case 2:
json = _a.sent();
this.userIdCache.set(site, json.Id);
return [2 /*return*/, json.Id];
}
});
});
};
DocumentsService.prototype.buildListUrl = function (siteUrl, listName) {
return "".concat(siteUrl, "/_api/web/lists/getByTitle('").concat(listName, "')/items");
};
DocumentsService.prototype.fetchItems = function (siteUrl, listName, params) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var q, url, response, json;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
q = [];
if (params.filter)
q.push("$filter=".concat(params.filter));
if (params.select)
q.push("$select=".concat(params.select));
if (params.expand)
q.push("$expand=".concat(params.expand));
if (params.orderby)
q.push("$orderby=".concat(params.orderby));
if (params.top !== undefined)
q.push("$top=".concat(params.top));
url = q.length > 0 ? "".concat(this.buildListUrl(siteUrl, listName), "?").concat(q.join('&')) : this.buildListUrl(siteUrl, listName);
return [4 /*yield*/, this.context.spHttpClient.get(url, sp_http_1.SPHttpClient.configurations.v1)];
case 1:
response = _a.sent();
if (!response.ok) {
throw new Error("\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u0441\u043F\u0438\u0441\u043A\u0430 \"".concat(listName, "\": ").concat(response.status));
}
return [4 /*yield*/, response.json()];
case 2:
json = _a.sent();
return [2 /*return*/, json.value];
}
});
});
};
DocumentsService.prototype.fetchMyDocs = function (config) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var siteUrl, userId, items;
var _this = this;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
siteUrl = this.siteUrlFor(config);
return [4 /*yield*/, this.resolveUserId(config)];
case 1:
userId = _a.sent();
return [4 /*yield*/, this.fetchItems(siteUrl, config.myDocsListName, {
filter: config.buildMyDocsFilter(userId),
select: config.myDocsSelect,
expand: config.myDocsExpand,
orderby: 'Created desc',
top: 100
})];
case 2:
items = _a.sent();
return [2 /*return*/, items.map(function (item) { return config.mapItem(item, _this.context); })];
}
});
});
};
DocumentsService.prototype.fetchApprovedByMe = function (config) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
return tslib_1.__generator(this, function (_a) {
if (!config.approvedByMe)
return [2 /*return*/, []];
return [2 /*return*/, this._fetchApprovalSection(config, config.approvedByMe)];
});
});
};
DocumentsService.prototype.fetchPendingApprovals = function (config) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
return tslib_1.__generator(this, function (_a) {
if (!config.pendingApproval)
return [2 /*return*/, []];
return [2 /*return*/, this._fetchApprovalSection(config, config.pendingApproval)];
});
});
};
DocumentsService.prototype._fetchApprovalSection = function (config, pa) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var siteUrl, userId, approvalItems, parentIds, parentQuery, parentItems;
var _this = this;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
siteUrl = this.siteUrlFor(config);
return [4 /*yield*/, this.resolveUserId(config)];
case 1:
userId = _a.sent();
return [4 /*yield*/, this.fetchItems(siteUrl, pa.approvalListName, {
filter: pa.buildPendingFilter(userId),
select: pa.approvalSelect,
expand: pa.approvalExpand,
top: 200
})];
case 2:
approvalItems = _a.sent();
parentIds = Array.from(new Set(approvalItems.map(function (item) { return Number(item[pa.parentIdField]); }).filter(function (id) { return !isNaN(id) && id > 0; })));
if (parentIds.length === 0)
return [2 /*return*/, []];
parentQuery = pa.buildParentQuery(parentIds);
return [4 /*yield*/, this.fetchItems(siteUrl, parentQuery.listName, {
filter: parentQuery.filter,
select: parentQuery.select,
top: 200
})];
case 3:
parentItems = _a.sent();
return [2 /*return*/, parentItems.map(function (item) { return config.mapItem(item, _this.context); })];
}
});
});
};
return DocumentsService;
}());
exports.DocumentsService = DocumentsService;
//# sourceMappingURL=documentsService.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.useDocumentDashboard = useDocumentDashboard;
var react_1 = require("react");
var documentsService_1 = require("./documentsService");
// Грузит три очереди (мои / согласованные / на согласовании) по всем типам сразу.
function useDocumentDashboard(context, documentTypes) {
var _a = (0, react_1.useState)([]), myDocs = _a[0], setMyDocs = _a[1];
var _b = (0, react_1.useState)([]), approvedDocs = _b[0], setApprovedDocs = _b[1];
var _c = (0, react_1.useState)([]), pendingDocs = _c[0], setPendingDocs = _c[1];
var _d = (0, react_1.useState)(true), isLoading = _d[0], setIsLoading = _d[1];
var _e = (0, react_1.useState)(null), error = _e[0], setError = _e[1];
var _f = (0, react_1.useState)(0), tick = _f[0], setTick = _f[1];
var reload = (0, react_1.useCallback)(function () { return setTick(function (t) { return t + 1; }); }, []);
(0, react_1.useEffect)(function () {
var service = new documentsService_1.DocumentsService(context);
setIsLoading(true);
setError(null);
Promise.all([
Promise.all(documentTypes.map(function (cfg) { return service.fetchMyDocs(cfg); })),
Promise.all(documentTypes.map(function (cfg) { return service.fetchApprovedByMe(cfg); })),
Promise.all(documentTypes.map(function (cfg) { return service.fetchPendingApprovals(cfg); }))
])
.then(function (_a) {
var myDocGroups = _a[0], approvedGroups = _a[1], pendingGroups = _a[2];
setMyDocs(myDocGroups.reduce(function (acc, arr) { return acc.concat(arr); }, []));
setApprovedDocs(approvedGroups.reduce(function (acc, arr) { return acc.concat(arr); }, []));
setPendingDocs(pendingGroups.reduce(function (acc, arr) { return acc.concat(arr); }, []));
setIsLoading(false);
})
.catch(function (err) {
var _a;
setError((_a = err.message) !== null && _a !== void 0 ? _a : 'Произошла ошибка при загрузке данных');
setIsLoading(false);
});
}, [context, tick]); // eslint-disable-line react-hooks/exhaustive-deps
return { myDocs: myDocs, approvedDocs: approvedDocs, pendingDocs: pendingDocs, isLoading: isLoading, error: error, reload: reload };
}
//# sourceMappingURL=useDocumentDashboard.js.map
@@ -0,0 +1 @@
{"version":3,"file":"useDocumentDashboard.js","sourceRoot":"","sources":["../../../../../src/webparts/ctFormsPortal/components/registry/useDocumentDashboard.ts"],"names":[],"mappings":";;AAeA,oDAoCC;AAnDD,+BAAyD;AAGzD,uDAAsD;AAWtD,kFAAkF;AAClF,SAAgB,oBAAoB,CAClC,OAAuB,EACvB,aAAmC;IAE7B,IAAA,KAAsB,IAAA,gBAAQ,EAAiB,EAAE,CAAC,EAAjD,MAAM,QAAA,EAAE,SAAS,QAAgC,CAAC;IACnD,IAAA,KAAkC,IAAA,gBAAQ,EAAiB,EAAE,CAAC,EAA7D,YAAY,QAAA,EAAE,eAAe,QAAgC,CAAC;IAC/D,IAAA,KAAgC,IAAA,gBAAQ,EAAiB,EAAE,CAAC,EAA3D,WAAW,QAAA,EAAE,cAAc,QAAgC,CAAC;IAC7D,IAAA,KAA4B,IAAA,gBAAQ,EAAU,IAAI,CAAC,EAAlD,SAAS,QAAA,EAAE,YAAY,QAA2B,CAAC;IACpD,IAAA,KAAoB,IAAA,gBAAQ,EAAgB,IAAI,CAAC,EAAhD,KAAK,QAAA,EAAE,QAAQ,QAAiC,CAAC;IAClD,IAAA,KAAkB,IAAA,gBAAQ,EAAS,CAAC,CAAC,EAApC,IAAI,QAAA,EAAE,OAAO,QAAuB,CAAC;IAE5C,IAAM,MAAM,GAAG,IAAA,mBAAW,EAAC,cAAM,OAAA,OAAO,CAAC,UAAC,CAAC,IAAK,OAAA,CAAC,GAAG,CAAC,EAAL,CAAK,CAAC,EAArB,CAAqB,EAAE,EAAE,CAAC,CAAC;IAE5D,IAAA,iBAAS,EAAC;QACR,IAAM,OAAO,GAAG,IAAI,mCAAgB,CAAC,OAAO,CAAC,CAAC;QAC9C,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,QAAQ,CAAC,IAAI,CAAC,CAAC;QAEf,OAAO,CAAC,GAAG,CAAC;YACV,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,UAAC,GAAG,IAAK,OAAA,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,EAAxB,CAAwB,CAAC,CAAC;YACjE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,UAAC,GAAG,IAAK,OAAA,OAAO,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAA9B,CAA8B,CAAC,CAAC;YACvE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,UAAC,GAAG,IAAK,OAAA,OAAO,CAAC,qBAAqB,CAAC,GAAG,CAAC,EAAlC,CAAkC,CAAC,CAAC;SAC5E,CAAC;aACC,IAAI,CAAC,UAAC,EAA4C;gBAA3C,WAAW,QAAA,EAAE,cAAc,QAAA,EAAE,aAAa,QAAA;YAChD,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC,UAAC,GAAG,EAAE,GAAG,IAAK,OAAA,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAf,CAAe,EAAE,EAAoB,CAAC,CAAC,CAAC;YACnF,eAAe,CAAC,cAAc,CAAC,MAAM,CAAC,UAAC,GAAG,EAAE,GAAG,IAAK,OAAA,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAf,CAAe,EAAE,EAAoB,CAAC,CAAC,CAAC;YAC5F,cAAc,CAAC,aAAa,CAAC,MAAM,CAAC,UAAC,GAAG,EAAE,GAAG,IAAK,OAAA,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAf,CAAe,EAAE,EAAoB,CAAC,CAAC,CAAC;YAC1F,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC,CAAC;aACD,KAAK,CAAC,UAAC,GAAU;;YAChB,QAAQ,CAAC,MAAA,GAAG,CAAC,OAAO,mCAAI,sCAAsC,CAAC,CAAC;YAChE,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC,CAAC,CAAC;IACP,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,kDAAkD;IAEvE,OAAO,EAAE,MAAM,QAAA,EAAE,YAAY,cAAA,EAAE,WAAW,aAAA,EAAE,SAAS,WAAA,EAAE,KAAK,OAAA,EAAE,MAAM,QAAA,EAAE,CAAC;AACzE,CAAC","sourcesContent":["import { useState, useEffect, useCallback } from 'react';\nimport { WebPartContext } from '@microsoft/sp-webpart-base';\nimport { DashboardRow, DocumentTypeConfig } from './documentRegistry';\nimport { DocumentsService } from './documentsService';\n\ninterface IDashboardState {\n myDocs: DashboardRow[];\n approvedDocs: DashboardRow[];\n pendingDocs: DashboardRow[];\n isLoading: boolean;\n error: string | null;\n reload: () => void;\n}\n\n// Грузит три очереди (мои / согласованные / на согласовании) по всем типам сразу.\nexport function useDocumentDashboard(\n context: WebPartContext,\n documentTypes: DocumentTypeConfig[]\n): IDashboardState {\n const [myDocs, setMyDocs] = useState<DashboardRow[]>([]);\n const [approvedDocs, setApprovedDocs] = useState<DashboardRow[]>([]);\n const [pendingDocs, setPendingDocs] = useState<DashboardRow[]>([]);\n const [isLoading, setIsLoading] = useState<boolean>(true);\n const [error, setError] = useState<string | null>(null);\n const [tick, setTick] = useState<number>(0);\n\n const reload = useCallback(() => setTick((t) => t + 1), []);\n\n useEffect(() => {\n const service = new DocumentsService(context);\n setIsLoading(true);\n setError(null);\n\n Promise.all([\n Promise.all(documentTypes.map((cfg) => service.fetchMyDocs(cfg))),\n Promise.all(documentTypes.map((cfg) => service.fetchApprovedByMe(cfg))),\n Promise.all(documentTypes.map((cfg) => service.fetchPendingApprovals(cfg)))\n ])\n .then(([myDocGroups, approvedGroups, pendingGroups]) => {\n setMyDocs(myDocGroups.reduce((acc, arr) => acc.concat(arr), [] as DashboardRow[]));\n setApprovedDocs(approvedGroups.reduce((acc, arr) => acc.concat(arr), [] as DashboardRow[]));\n setPendingDocs(pendingGroups.reduce((acc, arr) => acc.concat(arr), [] as DashboardRow[]));\n setIsLoading(false);\n })\n .catch((err: Error) => {\n setError(err.message ?? 'Произошла ошибка при загрузке данных');\n setIsLoading(false);\n });\n }, [context, tick]); // eslint-disable-line react-hooks/exhaustive-deps\n\n return { myDocs, approvedDocs, pendingDocs, isLoading, error, reload };\n}\n"]}
@@ -0,0 +1,16 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
"AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",
"AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook",
"AppSharePointEnvironment": "The app is running on SharePoint page",
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams",
"AppOfficeEnvironment": "The app is running in office.com",
"AppOutlookEnvironment": "The app is running in Outlook",
"UnknownEnvironment": "The app is running in an unknown environment"
}
});
+20078
View File
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
{
"name": "ct-forms-portal",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=22.14.0 < 23.0.0"
},
"scripts": {
"build": "heft test --clean --production && heft package-solution --production",
"start": "heft start --clean",
"clean": "heft clean",
"eject-webpack": "heft eject-webpack"
},
"dependencies": {
"@fluentui/react": "^8.106.4",
"@microsoft/sp-component-base": "1.22.1",
"@microsoft/sp-core-library": "1.22.1",
"@microsoft/sp-lodash-subset": "1.22.1",
"@microsoft/sp-office-ui-fabric-core": "1.22.1",
"@microsoft/sp-property-pane": "1.22.1",
"@microsoft/sp-webpart-base": "1.22.1",
"@tnsu/ui-kit": "^0.8.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"tslib": "2.3.1"
},
"devDependencies": {
"@microsoft/eslint-config-spfx": "1.22.2",
"@microsoft/eslint-plugin-spfx": "1.22.2",
"@microsoft/sp-module-interfaces": "1.22.2",
"@microsoft/spfx-heft-plugins": "1.22.2",
"@microsoft/spfx-web-build-rig": "1.22.2",
"@rushstack/eslint-config": "4.5.2",
"@rushstack/heft": "1.1.2",
"@types/heft-jest": "1.0.2",
"@types/react": "17.0.45",
"@types/react-dom": "17.0.17",
"@types/webpack-env": "~1.15.2",
"@typescript-eslint/parser": "8.46.2",
"css-loader": "~7.1.2",
"eslint": "8.57.1",
"eslint-plugin-react-hooks": "4.3.0",
"typescript": "~5.8.0"
},
"overrides": {
"@rushstack/heft": "1.1.2"
},
"resolutions": {
"@types/react": "17.0.45"
}
}
+742
View File
@@ -0,0 +1,742 @@
{
"version": "2.1.0",
"$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5",
"runs": [
{
"tool": {
"driver": {
"name": "ESLint",
"informationUri": "https://eslint.org",
"version": "8.57.1",
"rules": [
{
"id": "@rushstack/no-new-null",
"helpUri": "https://www.npmjs.com/package/@rushstack/eslint-plugin",
"properties": {},
"shortDescription": {
"text": "Prevent usage of JavaScript's \"null\" keyword in new type declarations. To avoid hampering usage of preexisting APIs that require \"null\", the rule ignores declarations that are local variables, private members, or types that are not exported."
}
},
{
"id": "dot-notation",
"helpUri": "https://eslint.org/docs/latest/rules/dot-notation",
"properties": {},
"shortDescription": {
"text": "Enforce dot notation whenever possible"
}
}
]
}
},
"artifacts": [
{
"location": {
"uri": "src/webparts/ctFormsPortal/components/registry/documentRegistry.ts"
}
},
{
"location": {
"uri": "src/webparts/ctFormsPortal/components/config/ctDocumentConfig.ts"
}
},
{
"location": {
"uri": "src/webparts/ctFormsPortal/components/config/oidDocumentConfig.ts"
}
},
{
"location": {
"uri": "src/webparts/ctFormsPortal/components/config/kpsDocumentConfig.ts"
}
},
{
"location": {
"uri": "src/webparts/ctFormsPortal/components/registry/documentsService.ts"
}
},
{
"location": {
"uri": "src/webparts/ctFormsPortal/components/registry/useDocumentDashboard.ts"
}
}
],
"results": [
{
"level": "warning",
"message": {
"text": "Usage of \"null\" is deprecated except when describing legacy APIs; use \"undefined\" instead"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/registry/documentRegistry.ts",
"index": 0
},
"region": {
"startLine": 45,
"startColumn": 44,
"endLine": 45,
"endColumn": 48,
"snippet": {
"text": "null"
}
}
}
}
],
"ruleId": "@rushstack/no-new-null",
"ruleIndex": 0
},
{
"level": "warning",
"message": {
"text": "Usage of \"null\" is deprecated except when describing legacy APIs; use \"undefined\" instead"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/registry/documentRegistry.ts",
"index": 0
},
"region": {
"startLine": 47,
"startColumn": 41,
"endLine": 47,
"endColumn": 45,
"snippet": {
"text": "null"
}
}
}
}
],
"ruleId": "@rushstack/no-new-null",
"ruleIndex": 0
},
{
"level": "warning",
"message": {
"text": "[\"status\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/ctDocumentConfig.ts",
"index": 1
},
"region": {
"startLine": 57,
"startColumn": 35,
"endLine": 57,
"endColumn": 43,
"snippet": {
"text": "'status'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Id\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/ctDocumentConfig.ts",
"index": 1
},
"region": {
"startLine": 60,
"startColumn": 24,
"endLine": 60,
"endColumn": 28,
"snippet": {
"text": "'Id'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"number\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/ctDocumentConfig.ts",
"index": 1
},
"region": {
"startLine": 61,
"startColumn": 27,
"endLine": 61,
"endColumn": 35,
"snippet": {
"text": "'number'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Title\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/ctDocumentConfig.ts",
"index": 1
},
"region": {
"startLine": 61,
"startColumn": 44,
"endLine": 61,
"endColumn": 51,
"snippet": {
"text": "'Title'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"project\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/ctDocumentConfig.ts",
"index": 1
},
"region": {
"startLine": 62,
"startColumn": 30,
"endLine": 62,
"endColumn": 39,
"snippet": {
"text": "'project'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Created\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/ctDocumentConfig.ts",
"index": 1
},
"region": {
"startLine": 66,
"startColumn": 26,
"endLine": 66,
"endColumn": 35,
"snippet": {
"text": "'Created'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Created\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/ctDocumentConfig.ts",
"index": 1
},
"region": {
"startLine": 66,
"startColumn": 50,
"endLine": 66,
"endColumn": 59,
"snippet": {
"text": "'Created'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"status\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/oidDocumentConfig.ts",
"index": 2
},
"region": {
"startLine": 63,
"startColumn": 35,
"endLine": 63,
"endColumn": 43,
"snippet": {
"text": "'status'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Id\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/oidDocumentConfig.ts",
"index": 2
},
"region": {
"startLine": 66,
"startColumn": 24,
"endLine": 66,
"endColumn": 28,
"snippet": {
"text": "'Id'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"number\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/oidDocumentConfig.ts",
"index": 2
},
"region": {
"startLine": 67,
"startColumn": 27,
"endLine": 67,
"endColumn": 35,
"snippet": {
"text": "'number'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Title\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/oidDocumentConfig.ts",
"index": 2
},
"region": {
"startLine": 67,
"startColumn": 44,
"endLine": 67,
"endColumn": 51,
"snippet": {
"text": "'Title'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"project\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/oidDocumentConfig.ts",
"index": 2
},
"region": {
"startLine": 68,
"startColumn": 30,
"endLine": 68,
"endColumn": 39,
"snippet": {
"text": "'project'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Created\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/oidDocumentConfig.ts",
"index": 2
},
"region": {
"startLine": 72,
"startColumn": 26,
"endLine": 72,
"endColumn": 35,
"snippet": {
"text": "'Created'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Created\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/oidDocumentConfig.ts",
"index": 2
},
"region": {
"startLine": 72,
"startColumn": 50,
"endLine": 72,
"endColumn": 59,
"snippet": {
"text": "'Created'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"status\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/kpsDocumentConfig.ts",
"index": 3
},
"region": {
"startLine": 61,
"startColumn": 35,
"endLine": 61,
"endColumn": 43,
"snippet": {
"text": "'status'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Id\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/kpsDocumentConfig.ts",
"index": 3
},
"region": {
"startLine": 64,
"startColumn": 24,
"endLine": 64,
"endColumn": 28,
"snippet": {
"text": "'Id'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"number\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/kpsDocumentConfig.ts",
"index": 3
},
"region": {
"startLine": 65,
"startColumn": 27,
"endLine": 65,
"endColumn": 35,
"snippet": {
"text": "'number'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Title\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/kpsDocumentConfig.ts",
"index": 3
},
"region": {
"startLine": 65,
"startColumn": 44,
"endLine": 65,
"endColumn": 51,
"snippet": {
"text": "'Title'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"project\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/kpsDocumentConfig.ts",
"index": 3
},
"region": {
"startLine": 66,
"startColumn": 30,
"endLine": 66,
"endColumn": 39,
"snippet": {
"text": "'project'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Created\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/kpsDocumentConfig.ts",
"index": 3
},
"region": {
"startLine": 70,
"startColumn": 26,
"endLine": 70,
"endColumn": 35,
"snippet": {
"text": "'Created'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"Created\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/config/kpsDocumentConfig.ts",
"index": 3
},
"region": {
"startLine": 70,
"startColumn": 50,
"endLine": 70,
"endColumn": 59,
"snippet": {
"text": "'Created'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "[\"userId\"] is better written in dot notation."
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/registry/documentsService.ts",
"index": 4
},
"region": {
"startLine": 13,
"startColumn": 91,
"endLine": 13,
"endColumn": 99,
"snippet": {
"text": "'userId'"
}
}
}
}
],
"ruleId": "dot-notation",
"ruleIndex": 1
},
{
"level": "warning",
"message": {
"text": "Usage of \"null\" is deprecated except when describing legacy APIs; use \"undefined\" instead"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "src/webparts/ctFormsPortal/components/registry/useDocumentDashboard.ts",
"index": 5
},
"region": {
"startLine": 11,
"startColumn": 19,
"endLine": 11,
"endColumn": 23,
"snippet": {
"text": "null"
}
}
}
}
],
"ruleId": "@rushstack/no-new-null",
"ruleIndex": 0
}
]
}
]
}
@@ -0,0 +1,16 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
"AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",
"AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook",
"AppSharePointEnvironment": "The app is running on SharePoint page",
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams",
"AppOfficeEnvironment": "The app is running in office.com",
"AppOutlookEnvironment": "The app is running in Outlook",
"UnknownEnvironment": "The app is running in an unknown environment"
}
});
@@ -0,0 +1,16 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
"AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",
"AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook",
"AppSharePointEnvironment": "The app is running on SharePoint page",
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams",
"AppOfficeEnvironment": "The app is running in office.com",
"AppOutlookEnvironment": "The app is running in Outlook",
"UnknownEnvironment": "The app is running in an unknown environment"
}
});
@@ -0,0 +1,102 @@
"use strict";
self["webpackHotUpdate_459579c7_954c_469e_b707_ca75d808603f_0_0_1"]("ct-forms-portal-web-part",{
/***/ 996:
/*!**************************************************************************!*\
!*** ./lib/webparts/ctFormsPortal/components/config/ctDocumentConfig.js ***!
\**************************************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ createCtDocumentConfig: () => (/* binding */ createCtDocumentConfig)
/* harmony export */ });
var STATUS_LABEL = {
'Черновик': 'Сформирована',
'На согласовании': 'На согласовании',
'Согласовано': 'Согласована',
'Отклонено': 'Отклонена'
};
var STATUS_VARIANT = {
'Черновик': 'neutral',
'На согласовании': 'info',
'Согласовано': 'success',
'Отклонено': 'error'
};
function createCtDocumentConfig() {
return {
typeKey: 'ct',
typeLabel: 'СТ ОМТС',
icon: 'ShoppingCart',
// Списки СТ ОМТС — на сайте СЭД. Явно, чтобы портал не зависел от места размещения.
siteUrl: 'https://tnsukz.sharepoint.com/sites/sed',
myDocsListName: 'Сравнительная таблица',
buildMyDocsFilter: function (userId) { return "AuthorId eq ".concat(userId); },
myDocsSelect: 'Id,Title,number,project,status,Created',
pendingApproval: {
approvalListName: 'Согласование СТ',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u041D\u0430 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u0438\u0438'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
approvedByMe: {
approvalListName: 'Согласование СТ',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u0421\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043E'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
buildLinkUrl: function (row) {
var base = 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица.aspx';
var param = row.status === 'Черновик' ? 'ctId' : 'ctid';
return "".concat(base, "?").concat(param, "=").concat(row.id);
},
createNewUrl: 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица.aspx',
createNewLabel: 'Создать новую форму СТ',
mapItem: function (raw) {
var _a, _b, _c, _d, _e;
var spStatus = String((_a = raw['status']) !== null && _a !== void 0 ? _a : '');
return {
typeKey: 'ct',
id: String(raw['Id']),
title: "\u0421\u0422 \u2116".concat((_b = raw['number']) !== null && _b !== void 0 ? _b : raw['Title']),
subtitle: String((_c = raw['project']) !== null && _c !== void 0 ? _c : ''),
status: spStatus,
statusLabel: (_d = STATUS_LABEL[spStatus]) !== null && _d !== void 0 ? _d : spStatus,
statusVariant: (_e = STATUS_VARIANT[spStatus]) !== null && _e !== void 0 ? _e : 'neutral',
createdDate: raw['Created'] ? String(raw['Created']) : undefined
};
},
columns: [
{ key: 'title', label: 'Номер СТ', minWidth: 120 },
{ key: 'subtitle', label: 'Проект', minWidth: 180 },
{ key: 'status', label: 'Статус', minWidth: 130 },
{ key: 'created', label: 'Создана', minWidth: 110 }
]
};
}
/***/ })
},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("c0c82c263a05c3417a10")
/******/ })();
/******/
/******/ }
);
//# sourceMappingURL=ct-forms-portal-web-part.8bc87a793f5dc3d11d32.hot-update.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"c":["ct-forms-portal-web-part"],"r":[],"m":[]}
@@ -0,0 +1,108 @@
"use strict";
self["webpackHotUpdate_459579c7_954c_469e_b707_ca75d808603f_0_0_1"]("ct-forms-portal-web-part",{
/***/ 633:
/*!***************************************************************************!*\
!*** ./lib/webparts/ctFormsPortal/components/config/oidDocumentConfig.js ***!
\***************************************************************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ createOidDocumentConfig: () => (/* binding */ createOidDocumentConfig)
/* harmony export */ });
var STATUS_LABEL = {
'Черновик': 'Сформирована',
'formed': 'Сформирована',
'На согласовании': 'На согласовании',
'on_approval': 'На согласовании',
'Согласовано': 'Согласована',
'approved': 'Согласована',
'Отклонено': 'Отклонена'
};
var STATUS_VARIANT = {
'Черновик': 'neutral',
'formed': 'neutral',
'На согласовании': 'info',
'on_approval': 'info',
'Согласовано': 'success',
'approved': 'success',
'Отклонено': 'error'
};
function createOidDocumentConfig() {
return {
typeKey: 'oid',
typeLabel: 'СТ ОИД',
icon: 'CRMServices',
// Списки СТ ОИД — на сайте СЭД. Явно, чтобы портал не зависел от места размещения.
siteUrl: 'https://tnsukz.sharepoint.com/sites/sed',
myDocsListName: 'Сравнительная таблица ОИД',
buildMyDocsFilter: function (userId) { return "AuthorId eq ".concat(userId); },
myDocsSelect: 'Id,Title,number,project,status,Created',
pendingApproval: {
approvalListName: 'Согласование СТ ОИД',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u041D\u0430 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u0438\u0438'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица ОИД',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
approvedByMe: {
approvalListName: 'Согласование СТ ОИД',
buildPendingFilter: function (userId) { return "aproover/Id eq ".concat(userId, " and status eq '\u0421\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043E'"); },
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: function (parentIds) { return ({
listName: 'Сравнительная таблица ОИД',
filter: parentIds.map(function (id) { return "Id eq ".concat(id); }).join(' or '),
select: 'Id,Title,number,project,status,Created'
}); }
},
buildLinkUrl: function (row) {
var base = 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица-ОИД.aspx';
var param = row.status === 'Черновик' || row.status === 'formed' ? 'oidId' : 'oidid';
return "".concat(base, "?").concat(param, "=").concat(row.id);
},
createNewUrl: 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица-ОИД.aspx',
createNewLabel: 'Создать новую форму ОИД',
mapItem: function (raw) {
var _a, _b, _c, _d, _e;
var spStatus = String((_a = raw['status']) !== null && _a !== void 0 ? _a : '');
return {
typeKey: 'oid',
id: String(raw['Id']),
title: "\u0421\u0422 \u2116".concat((_b = raw['number']) !== null && _b !== void 0 ? _b : raw['Title']),
subtitle: String((_c = raw['project']) !== null && _c !== void 0 ? _c : ''),
status: spStatus,
statusLabel: (_d = STATUS_LABEL[spStatus]) !== null && _d !== void 0 ? _d : spStatus,
statusVariant: (_e = STATUS_VARIANT[spStatus]) !== null && _e !== void 0 ? _e : 'neutral',
createdDate: raw['Created'] ? String(raw['Created']) : undefined
};
},
columns: [
{ key: 'title', label: 'Номер СТ', minWidth: 120 },
{ key: 'subtitle', label: 'Проект', minWidth: 180 },
{ key: 'status', label: 'Статус', minWidth: 130 },
{ key: 'created', label: 'Создана', minWidth: 110 }
]
};
}
/***/ })
},
/******/ function(__webpack_require__) { // webpackRuntimeModules
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("28bba40dc5ac93a046a4")
/******/ })();
/******/
/******/ }
);
//# sourceMappingURL=ct-forms-portal-web-part.c0c82c263a05c3417a10.hot-update.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"c":["ct-forms-portal-web-part"],"r":[],"m":[]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
/** @license React v17.0.1
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
@@ -0,0 +1,237 @@
{
"bundles": {
"ct-forms-portal-web-part": {
"dependencies": [
{
"componentId": "0d910c1c-13b9-4e1c-9aa4-b008c5e42d7d",
"componentName": "react",
"componentVersion": "17.0.1",
"isDirectDependency": true
},
{
"componentId": "aa0a46ec-1505-43cd-a44a-93f3a5aa460a",
"componentName": "react-dom",
"componentVersion": "17.0.1",
"isDirectDependency": true
},
{
"componentId": "7263c7d0-1d6a-45ec-8d85-d4d1d234171b",
"componentName": "@microsoft/sp-core-library",
"componentVersion": "1.22.1",
"isDirectDependency": true
},
{
"componentId": "01c4df03-e775-48cb-aa14-171ee5199a15",
"componentName": "tslib",
"componentVersion": "2.3.1",
"isDirectDependency": false
},
{
"componentId": "2e09fb9b-13bb-48f2-859f-97d6fff71176",
"componentName": "@msinternal/odsp-core-bundle",
"componentVersion": "1.4.517",
"isDirectDependency": false
},
{
"componentId": "f9e737b7-f0df-4597-ba8c-3060f82380db",
"componentName": "@microsoft/sp-property-pane",
"componentVersion": "1.22.1",
"isDirectDependency": true
},
{
"componentId": "78359e4b-07c2-43c6-8d0b-d060b4d577e8",
"componentName": "@microsoft/sp-diagnostics",
"componentVersion": "1.22.1",
"isDirectDependency": false
},
{
"componentId": "73e1dc6c-8441-42cc-ad47-4bd3659f8a3a",
"componentName": "@microsoft/sp-lodash-subset",
"componentVersion": "1.22.1",
"isDirectDependency": false
},
{
"componentId": "3173fefe-9995-49d1-a75d-a5caeaaba212",
"componentName": "@msinternal/sp-pages-content-panel-context",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "229b8d08-79f3-438b-8c21-4613fc877abd",
"componentName": "@microsoft/load-themed-styles",
"componentVersion": "0.1.2",
"isDirectDependency": false
},
{
"componentId": "8494e7d7-6b99-47b2-a741-59873e42f16f",
"componentName": "@msinternal/sp-component-utilities",
"componentVersion": "13.12.19",
"isDirectDependency": false
},
{
"componentId": "63d75709-2785-4c91-97ad-489b171397d4",
"componentName": "@msinternal/sp-fluentui-migration-reactdom-render-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "cc2cc925-b5be-41bb-880a-f0f8030c6aff",
"componentName": "@msinternal/odsp-utilities-bundle",
"componentVersion": "5.6.517",
"isDirectDependency": false
},
{
"componentId": "8217e442-8ed3-41fd-957d-b112e841286a",
"componentName": "@msinternal/sp-telemetry",
"componentVersion": "0.35.58",
"isDirectDependency": false
},
{
"componentId": "0ec74f52-38bc-4a51-ab82-7c91a2b399a2",
"componentName": "@msinternal/sp-fluentui-v9-utilities-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "72ec0651-8a26-441c-87f2-77ec66884ca3",
"componentName": "@msinternal/sp-fluentui-v9-react-button-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "f652a763-4d5f-4a08-aed3-d3377ef835ff",
"componentName": "@msinternal/sp-fluentui-v9-react-menu-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "d3139516-bb4d-4141-9d35-a8aefe0272ec",
"componentName": "@msinternal/sp-fluentui-v9-react-field-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "17f3a86d-33ce-4338-8594-8700ee90c494",
"componentName": "@msinternal/sp-fluentui-v9-react-infolabel-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "33c5a798-8532-4741-888a-d0a7734036b5",
"componentName": "@msinternal/sp-fluentui-v9-react-tooltip-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "29728f96-9823-40fa-a481-ffdf106880b1",
"componentName": "@msinternal/sp-fluentui-v9-react-combobox-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "dbdce381-c024-42a0-ab20-4134aca49a27",
"componentName": "@msinternal/sp-fluentui-v9-react-label-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "396bbc17-3d18-4f43-8299-d99720d88e22",
"componentName": "@ms/office-ui-fabric-react-bundle",
"componentVersion": "1.19.51",
"isDirectDependency": false
},
{
"componentId": "fab65e60-a04f-4772-874c-10691e4afa4b",
"componentName": "@msinternal/sp-fluentui-migration-tooltip-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "0ad7e31a-8df9-4659-add8-a32c8e40040d",
"componentName": "@msinternal/sp-fluentui-migration-button-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "bf211556-3cf0-412c-9618-a4f0e7069ebf",
"componentName": "@msinternal/sp-heading-level",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "ffe5c101-373a-4bd2-bebb-13082852bab0",
"componentName": "@msinternal/sp-fluentui-migration-label-bundle",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "1c6c9123-7aac-41f3-a376-3caea41ed83f",
"componentName": "@microsoft/sp-loader",
"componentVersion": "1.22.1",
"isDirectDependency": false
},
{
"componentId": "8496636c-2300-4915-abef-20de64c98d8b",
"componentName": "@microsoft/sp-http-base",
"componentVersion": "1.22.1",
"isDirectDependency": false
},
{
"componentId": "1c4541f7-5c31-41aa-9fa8-fbc9dc14c0a8",
"componentName": "@microsoft/sp-page-context",
"componentVersion": "1.22.1",
"isDirectDependency": false
},
{
"componentId": "e40f8203-b39d-425a-a957-714852e33b79",
"componentName": "@microsoft/sp-dynamic-data",
"componentVersion": "1.22.1",
"isDirectDependency": false
},
{
"componentId": "974a7777-0990-4136-8fa6-95d80114c2e0",
"componentName": "@microsoft/sp-webpart-base",
"componentVersion": "1.22.1",
"isDirectDependency": true
},
{
"componentId": "467dc675-7cc5-4709-8aac-78e3b71bd2f6",
"componentName": "@microsoft/sp-component-base",
"componentVersion": "1.22.1",
"isDirectDependency": false
},
{
"componentId": "c07208f0-ea3b-4c1a-9965-ac1b825211a6",
"componentName": "@microsoft/sp-http",
"componentVersion": "1.22.1",
"isDirectDependency": false
},
{
"componentId": "aec2bbfd-a968-4d5b-a279-12e570f378e6",
"componentName": "@microsoft/sp-http-msgraph",
"componentVersion": "1.22.1",
"isDirectDependency": false
},
{
"componentId": "6d3c733c-8cb7-4871-bb22-b5b37c1d518d",
"componentName": "@msinternal/sp-safehtml",
"componentVersion": "0.5.2",
"isDirectDependency": false
},
{
"componentId": "42647dee-1f75-4b9f-b521-5c844a6c35cc",
"componentName": "@msinternal/sp-edit-customer-promise",
"componentVersion": "0.1.0",
"isDirectDependency": false
},
{
"componentId": "c49ba483-7614-4c5c-b9dd-11b45a5fa280",
"componentName": "@msinternal/sp-idle-web-task",
"componentVersion": "0.1.2",
"isDirectDependency": false
}
],
"asyncChunks": {}
}
}
}
@@ -0,0 +1,80 @@
{
"id": "459579c7-954c-469e-b707-ca75d808603f",
"alias": "CtFormsPortalWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"requiresCustomScript": false,
"supportedHosts": [
"SharePointWebPart",
"TeamsPersonalApp",
"TeamsTab",
"SharePointFullPage"
],
"supportsThemeVariants": true,
"supportsFullBleed": true,
"preconfiguredEntries": [
{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": {
"default": "Advanced"
},
"title": {
"default": "ct-forms-portal"
},
"description": {
"default": "ct-forms-portal description"
},
"officeFabricIconFontName": "Page",
"properties": {
"description": "ct-forms-portal"
}
}
],
"loaderConfig": {
"internalModuleBaseUrls": [
"<!-- PATH TO CDN -->"
],
"entryModuleId": "ct-forms-portal-web-part",
"scriptResources": {
"ct-forms-portal-web-part": {
"type": "path",
"path": "ct-forms-portal-web-part.js"
},
"react": {
"type": "component",
"id": "0d910c1c-13b9-4e1c-9aa4-b008c5e42d7d",
"version": "17.0.1"
},
"react-dom": {
"type": "component",
"id": "aa0a46ec-1505-43cd-a44a-93f3a5aa460a",
"version": "17.0.1"
},
"@microsoft/sp-core-library": {
"type": "component",
"id": "7263c7d0-1d6a-45ec-8d85-d4d1d234171b",
"version": "1.22.1"
},
"@microsoft/sp-property-pane": {
"type": "component",
"id": "f9e737b7-f0df-4597-ba8c-3060f82380db",
"version": "1.22.1"
},
"@microsoft/sp-webpart-base": {
"type": "component",
"id": "974a7777-0990-4136-8fa6-95d80114c2e0",
"version": "1.22.1"
},
"CtFormsPortalWebPartStrings": {
"type": "path",
"path": "CtFormsPortalWebPartStrings_en-us.js"
},
"@microsoft/sp-http": {
"type": "component",
"id": "c07208f0-ea3b-4c1a-9965-ac1b825211a6",
"version": "1.22.1"
}
}
}
}
@@ -0,0 +1,29 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "459579c7-954c-469e-b707-ca75d808603f",
"alias": "CtFormsPortalWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"],
"supportsThemeVariants": true,
"supportsFullBleed": true,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced
"group": { "default": "Advanced" },
"title": { "default": "ct-forms-portal" },
"description": { "default": "ct-forms-portal description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "ct-forms-portal"
}
}]
}
@@ -0,0 +1,122 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
type IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import * as strings from 'CtFormsPortalWebPartStrings';
import CtFormsPortal from './components/CtFormsPortal';
import { ICtFormsPortalProps } from './components/ICtFormsPortalProps';
export interface ICtFormsPortalWebPartProps {
description: string;
}
export default class CtFormsPortalWebPart extends BaseClientSideWebPart<ICtFormsPortalWebPartProps> {
private _isDarkTheme: boolean = false;
private _environmentMessage: string = '';
public render(): void {
const element: React.ReactElement<ICtFormsPortalProps> = React.createElement(
CtFormsPortal,
{
description: this.properties.description,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName,
context: this.context
}
);
ReactDom.render(element, this.domElement);
}
protected onInit(): Promise<void> {
return this._getEnvironmentMessage().then(message => {
this._environmentMessage = message;
});
}
private _getEnvironmentMessage(): Promise<string> {
if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
.then(context => {
let environmentMessage: string = '';
switch (context.app.host.name) {
case 'Office': // running in Office
environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;
break;
case 'Outlook': // running in Outlook
environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;
break;
case 'Teams': // running in Teams
case 'TeamsModern':
environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
break;
default:
environmentMessage = strings.UnknownEnvironment;
}
return environmentMessage;
});
}
return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
const {
semanticColors
} = currentTheme;
if (semanticColors) {
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null);
this.domElement.style.setProperty('--link', semanticColors.link || null);
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null);
}
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,17 @@
// Full-width: снимаем max-width канвы SharePoint — и в workbench (dev), и на живой
// странице. !important оправдан: перебиваем встроенные стили канвы, иначе их не снять.
// Требует "supportsFullBleed": true в манифесте + размещение в полноширинной секции.
// Если после обновления платформы ширина вернётся — проверить актуальные селекторы в DevTools.
:global {
#workbenchPageContent,
#spPageCanvasContent,
.SPCanvas-canvas,
.CanvasComponent.LCS .CanvasZone,
.CanvasZone {
max-width: 100% !important;
}
}
.root {
width: 100%;
}
@@ -0,0 +1,17 @@
import * as React from 'react';
import '@tnsu/ui-kit/styles.css';
import { IconSprite } from '@tnsu/ui-kit';
import styles from './CtFormsPortal.module.scss';
import type { ICtFormsPortalProps } from './ICtFormsPortalProps';
import { Dashboard } from './Dashboard';
// Корень веб-парта: спрайт иконок (один раз) + дашборд-реестр.
// Импорт CtFormsPortal.module.scss обязателен — в нём :global full-width правила канвы.
const CtFormsPortal: React.FC<ICtFormsPortalProps> = (props) => (
<div className={styles.root}>
<IconSprite />
<Dashboard context={props.context} />
</div>
);
export default CtFormsPortal;
@@ -0,0 +1,57 @@
// Раскладку (сайдбар+контент) даёт @tnsu/ui-kit AppLayout. Здесь — только
// оформление содержимого реестра (заголовок, тулбар, карточка таблицы).
.head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.title {
margin: 0;
font-family: 'Lora', Georgia, serif;
font-size: 22px;
font-weight: 600;
color: #0f2440;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.search {
width: 280px;
max-width: 100%;
}
.filter {
width: 200px;
}
.tableWrap {
margin-top: 16px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
box-shadow: 0 1px 2px rgba(15, 36, 64, 0.04), 0 1px 3px rgba(15, 36, 64, 0.06);
overflow: hidden;
}
.state {
margin-top: 16px;
color: #6b7280;
font-size: 13px;
}
@media (max-width: 640px) {
.search,
.filter {
width: 100%;
}
}
@@ -0,0 +1,171 @@
import * as React from 'react';
import { useState } from 'react';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import styles from './Dashboard.module.scss';
import {
AppLayout,
Sidebar,
SidebarToggle,
ISidebarItem,
Tabs,
ITab,
DataTable,
IDataTableColumn,
Badge,
BadgeVariant,
Button,
TextInput,
Select,
ISelectOption,
Alert,
IconName
} from '@tnsu/ui-kit';
import { DOCUMENT_TYPES } from './config';
import { useDocumentDashboard } from './registry/useDocumentDashboard';
import { DashboardRow } from './registry/documentRegistry';
type QueueKey = 'pending' | 'mine' | 'approved';
// Иконка сайдбара по типу (kit-иконки; Fluent-иконки из конфигов тут не используем).
const TYPE_ICON: Record<string, IconName> = { ct: 'clipboard', oid: 'chart', kps: 'building' };
// Цвет статуса DashboardRow → вариант нашего Badge (error → danger).
const VARIANT_MAP: Record<DashboardRow['statusVariant'], BadgeVariant> = {
neutral: 'neutral',
info: 'info',
success: 'success',
error: 'danger'
};
function formatDate(iso?: string): string {
if (!iso) return '';
const d = new Date(iso);
const dd = d.getDate() < 10 ? `0${d.getDate()}` : `${d.getDate()}`;
const mm = d.getMonth() + 1 < 10 ? `0${d.getMonth() + 1}` : `${d.getMonth() + 1}`;
return `${dd}.${mm}.${d.getFullYear()}`;
}
// Дашборд-реестр (config-driven, данные из SharePoint). Оболочка и элементы — из @tnsu/ui-kit.
export const Dashboard: React.FC<{ context: WebPartContext }> = ({ context }) => {
const { myDocs, approvedDocs, pendingDocs, isLoading, error } = useDocumentDashboard(context, DOCUMENT_TYPES);
const [activeType, setActiveType] = useState<string>(DOCUMENT_TYPES[0].typeKey);
const [queue, setQueue] = useState<QueueKey>('pending');
const [collapsed, setCollapsed] = useState<boolean>(false);
const [mobileOpen, setMobileOpen] = useState<boolean>(false);
const [search, setSearch] = useState<string>('');
const [status, setStatus] = useState<string>('');
const cfg = DOCUMENT_TYPES.filter((t) => t.typeKey === activeType)[0];
const byType = (arr: DashboardRow[]): DashboardRow[] => arr.filter((r) => r.typeKey === activeType);
const pendingForType = byType(pendingDocs);
const mineForType = byType(myDocs);
const approvedForType = byType(approvedDocs);
const sidebarItems: ISidebarItem[] = DOCUMENT_TYPES.map((t) => ({
id: t.typeKey,
title: t.typeLabel,
icon: TYPE_ICON[t.typeKey] ?? 'document',
count: pendingDocs.filter((r) => r.typeKey === t.typeKey).length
}));
const tabs: ITab[] = [
{ id: 'pending', label: 'На согласовании у меня', count: pendingForType.length },
{ id: 'mine', label: 'Инициализированные мной', count: mineForType.length },
{ id: 'approved', label: 'Согласованные мной', count: approvedForType.length }
];
const activeRows = queue === 'pending' ? pendingForType : queue === 'mine' ? mineForType : approvedForType;
const statusOptions: ISelectOption[] = Array.from(new Set(activeRows.map((r) => r.statusLabel))).map((s) => ({
value: s,
label: s
}));
const rows = activeRows
.filter((r) => !search || `${r.title} ${r.subtitle ?? ''}`.toLowerCase().indexOf(search.toLowerCase()) !== -1)
.filter((r) => !status || r.statusLabel === status);
const renderCell = (row: DashboardRow, key: string): React.ReactNode => {
if (key === 'status') {
return (
<Badge variant={VARIANT_MAP[row.statusVariant]} size="sm">
{row.statusLabel}
</Badge>
);
}
if (key === 'created') return formatDate(row.createdDate);
return String((row as unknown as Record<string, unknown>)[key] ?? '');
};
const columns: IDataTableColumn<DashboardRow>[] = cfg.columns.map((c) => ({
key: c.key,
title: c.label,
width: c.minWidth ? `${c.minWidth}px` : undefined,
render: (row) => renderCell(row, c.key)
}));
const selectType = (id: string | number): void => {
setActiveType(String(id));
setQueue('pending');
setSearch('');
setStatus('');
};
return (
<AppLayout
sidebar={
<Sidebar
items={sidebarItems}
activeId={activeType}
onChange={selectType}
brand="Договора"
collapsed={collapsed}
onToggleCollapsed={() => setCollapsed((c) => !c)}
mobileOpen={mobileOpen}
onMobileClose={() => setMobileOpen(false)}
/>
}
mobileToggle={<SidebarToggle onClick={() => setMobileOpen(true)} />}
>
<div className={styles.head}>
<h2 className={styles.title}>{cfg.typeLabel}</h2>
{cfg.createNewUrl && (
<Button variant="accent" onClick={() => window.open(cfg.createNewUrl, '_blank')}>
{cfg.createNewLabel ?? 'Создать'}
</Button>
)}
</div>
<div className={styles.toolbar}>
<div className={styles.search}>
<TextInput value={search} onChange={setSearch} placeholder="Поиск по номеру или проекту" />
</div>
<div className={styles.filter}>
<Select value={status} onChange={setStatus} options={statusOptions} placeholder="Все статусы" />
</div>
</div>
<Tabs tabs={tabs} activeId={queue} onChange={(id) => setQueue(id as QueueKey)} />
{error && (
<div className={styles.state}>
<Alert variant="err">Не удалось загрузить данные: {error}</Alert>
</div>
)}
{!error && isLoading && <div className={styles.state}>Загрузка…</div>}
{!error && !isLoading && (
<div className={styles.tableWrap}>
<DataTable
columns={columns}
rows={rows}
rowKey={(r) => `${r.typeKey}-${r.id}`}
onRowClick={(r) => window.open(cfg.buildLinkUrl(r), '_blank')}
emptyText="Нет документов"
/>
</div>
)}
</AppLayout>
);
};
@@ -0,0 +1,10 @@
import { WebPartContext } from '@microsoft/sp-webpart-base';
export interface ICtFormsPortalProps {
description: string;
isDarkTheme: boolean;
environmentMessage: string;
hasTeamsContext: boolean;
userDisplayName: string;
context: WebPartContext;
}
@@ -0,0 +1,78 @@
import { DocumentTypeConfig, DashboardRow } from '../registry/documentRegistry';
const STATUS_LABEL: Record<string, string> = {
'Черновик': 'Сформирована',
'На согласовании': 'На согласовании',
'Согласовано': 'Согласована',
'Отклонено': 'Отклонена'
};
const STATUS_VARIANT: Record<string, DashboardRow['statusVariant']> = {
'Черновик': 'neutral',
'На согласовании': 'info',
'Согласовано': 'success',
'Отклонено': 'error'
};
export function createCtDocumentConfig(): DocumentTypeConfig {
return {
typeKey: 'ct',
typeLabel: 'СТ ОМТС',
icon: 'ShoppingCart',
// Списки СТ ОМТС — на сайте СЭД. Явно, чтобы портал не зависел от места размещения.
siteUrl: 'https://tnsukz.sharepoint.com/sites/sed',
myDocsListName: 'Сравнительная таблица',
buildMyDocsFilter: (userId) => `AuthorId eq ${userId}`,
myDocsSelect: 'Id,Title,number,project,status,Created',
pendingApproval: {
approvalListName: 'Согласование СТ',
buildPendingFilter: (userId) => `aproover/Id eq ${userId} and status eq 'На согласовании'`,
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: (parentIds) => ({
listName: 'Сравнительная таблица',
filter: parentIds.map((id) => `Id eq ${id}`).join(' or '),
select: 'Id,Title,number,project,status,Created'
})
},
approvedByMe: {
approvalListName: 'Согласование СТ',
buildPendingFilter: (userId) => `aproover/Id eq ${userId} and status eq 'Согласовано'`,
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: (parentIds) => ({
listName: 'Сравнительная таблица',
filter: parentIds.map((id) => `Id eq ${id}`).join(' or '),
select: 'Id,Title,number,project,status,Created'
})
},
buildLinkUrl: (row) => {
const base = 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица.aspx';
const param = row.status === 'Черновик' ? 'ctId' : 'ctid';
return `${base}?${param}=${row.id}`;
},
createNewUrl: 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица.aspx',
createNewLabel: 'Создать новую форму СТ',
mapItem: (raw): DashboardRow => {
const spStatus = String(raw['status'] ?? '');
return {
typeKey: 'ct',
id: String(raw['Id']),
title: `СТ №${raw['number'] ?? raw['Title']}`,
subtitle: String(raw['project'] ?? ''),
status: spStatus,
statusLabel: STATUS_LABEL[spStatus] ?? spStatus,
statusVariant: STATUS_VARIANT[spStatus] ?? 'neutral',
createdDate: raw['Created'] ? String(raw['Created']) : undefined
};
},
columns: [
{ key: 'title', label: 'Номер СТ', minWidth: 120 },
{ key: 'subtitle', label: 'Проект', minWidth: 180 },
{ key: 'status', label: 'Статус', minWidth: 130 },
{ key: 'created', label: 'Создана', minWidth: 110 }
]
};
}
@@ -0,0 +1,11 @@
import { DocumentTypeConfig } from '../registry/documentRegistry';
import { createCtDocumentConfig } from './ctDocumentConfig';
import { createOidDocumentConfig } from './oidDocumentConfig';
import { createKpsDocumentConfig } from './kpsDocumentConfig';
// Все типы документов портала. Добавить тип = добавить конфиг сюда.
export const DOCUMENT_TYPES: DocumentTypeConfig[] = [
createCtDocumentConfig(),
createOidDocumentConfig(),
createKpsDocumentConfig()
];
@@ -0,0 +1,80 @@
import { DocumentTypeConfig, DashboardRow } from '../registry/documentRegistry';
const KPS_SITE_URL = 'https://tnsukz.sharepoint.com/sites/kps';
const KPS_FORM_PAGE = `${KPS_SITE_URL}/SitePages/Сравнительная-таблица.aspx`;
const STATUS_LABEL: Record<string, string> = {
'Черновик': 'Сформирована',
'На согласовании': 'На согласовании',
'Согласовано': 'Согласована',
'Отклонено': 'Отклонена'
};
const STATUS_VARIANT: Record<string, DashboardRow['statusVariant']> = {
'Черновик': 'neutral',
'На согласовании': 'info',
'Согласовано': 'success',
'Отклонено': 'error'
};
export function createKpsDocumentConfig(): DocumentTypeConfig {
return {
typeKey: 'kps',
typeLabel: 'СТ КПС',
icon: 'Processing',
// Списки КПС — на отдельном сайт-коллекшене /sites/kps.
siteUrl: KPS_SITE_URL,
myDocsListName: 'Сравнительная таблица',
buildMyDocsFilter: (userId) => `AuthorId eq ${userId}`,
myDocsSelect: 'Id,Title,number,project,status,Created',
pendingApproval: {
approvalListName: 'Согласование СТ',
buildPendingFilter: (userId) => `aproover/Id eq ${userId} and status eq 'На согласовании'`,
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: (parentIds) => ({
listName: 'Сравнительная таблица',
filter: parentIds.map((id) => `Id eq ${id}`).join(' or '),
select: 'Id,Title,number,project,status,Created'
})
},
approvedByMe: {
approvalListName: 'Согласование СТ',
buildPendingFilter: (userId) => `aproover/Id eq ${userId} and status eq 'Согласовано'`,
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: (parentIds) => ({
listName: 'Сравнительная таблица',
filter: parentIds.map((id) => `Id eq ${id}`).join(' or '),
select: 'Id,Title,number,project,status,Created'
})
},
buildLinkUrl: (row) => {
const param = row.status === 'Черновик' ? 'ctId' : 'ctid';
return `${KPS_FORM_PAGE}?${param}=${row.id}`;
},
createNewUrl: KPS_FORM_PAGE,
createNewLabel: 'Создать новую форму КПС',
mapItem: (raw): DashboardRow => {
const spStatus = String(raw['status'] ?? '');
return {
typeKey: 'kps',
id: String(raw['Id']),
title: `СТ №${raw['number'] ?? raw['Title']}`,
subtitle: String(raw['project'] ?? ''),
status: spStatus,
statusLabel: STATUS_LABEL[spStatus] ?? spStatus,
statusVariant: STATUS_VARIANT[spStatus] ?? 'neutral',
createdDate: raw['Created'] ? String(raw['Created']) : undefined
};
},
columns: [
{ key: 'title', label: 'Номер СТ', minWidth: 120 },
{ key: 'subtitle', label: 'Проект', minWidth: 180 },
{ key: 'status', label: 'Статус', minWidth: 130 },
{ key: 'created', label: 'Создана', minWidth: 110 }
]
};
}
@@ -0,0 +1,84 @@
import { DocumentTypeConfig, DashboardRow } from '../registry/documentRegistry';
const STATUS_LABEL: Record<string, string> = {
'Черновик': 'Сформирована',
'formed': 'Сформирована',
'На согласовании': 'На согласовании',
'on_approval': 'На согласовании',
'Согласовано': 'Согласована',
'approved': 'Согласована',
'Отклонено': 'Отклонена'
};
const STATUS_VARIANT: Record<string, DashboardRow['statusVariant']> = {
'Черновик': 'neutral',
'formed': 'neutral',
'На согласовании': 'info',
'on_approval': 'info',
'Согласовано': 'success',
'approved': 'success',
'Отклонено': 'error'
};
export function createOidDocumentConfig(): DocumentTypeConfig {
return {
typeKey: 'oid',
typeLabel: 'СТ ОИД',
icon: 'CRMServices',
// Списки СТ ОИД — на сайте СЭД. Явно, чтобы портал не зависел от места размещения.
siteUrl: 'https://tnsukz.sharepoint.com/sites/sed',
myDocsListName: 'Сравнительная таблица ОИД',
buildMyDocsFilter: (userId) => `AuthorId eq ${userId}`,
myDocsSelect: 'Id,Title,number,project,status,Created',
pendingApproval: {
approvalListName: 'Согласование СТ ОИД',
buildPendingFilter: (userId) => `aproover/Id eq ${userId} and status eq 'На согласовании'`,
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: (parentIds) => ({
listName: 'Сравнительная таблица ОИД',
filter: parentIds.map((id) => `Id eq ${id}`).join(' or '),
select: 'Id,Title,number,project,status,Created'
})
},
approvedByMe: {
approvalListName: 'Согласование СТ ОИД',
buildPendingFilter: (userId) => `aproover/Id eq ${userId} and status eq 'Согласовано'`,
approvalSelect: 'Id,CTId,status,aproover/Id',
approvalExpand: 'aproover',
parentIdField: 'CTId',
buildParentQuery: (parentIds) => ({
listName: 'Сравнительная таблица ОИД',
filter: parentIds.map((id) => `Id eq ${id}`).join(' or '),
select: 'Id,Title,number,project,status,Created'
})
},
buildLinkUrl: (row) => {
const base = 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица-ОИД.aspx';
const param = row.status === 'Черновик' || row.status === 'formed' ? 'oidId' : 'oidid';
return `${base}?${param}=${row.id}`;
},
createNewUrl: 'https://tnsukz.sharepoint.com/sites/sed/SitePages/Сравнительная-таблица-ОИД.aspx',
createNewLabel: 'Создать новую форму ОИД',
mapItem: (raw): DashboardRow => {
const spStatus = String(raw['status'] ?? '');
return {
typeKey: 'oid',
id: String(raw['Id']),
title: `СТ №${raw['number'] ?? raw['Title']}`,
subtitle: String(raw['project'] ?? ''),
status: spStatus,
statusLabel: STATUS_LABEL[spStatus] ?? spStatus,
statusVariant: STATUS_VARIANT[spStatus] ?? 'neutral',
createdDate: raw['Created'] ? String(raw['Created']) : undefined
};
},
columns: [
{ key: 'title', label: 'Номер СТ', minWidth: 120 },
{ key: 'subtitle', label: 'Проект', minWidth: 180 },
{ key: 'status', label: 'Статус', minWidth: 130 },
{ key: 'created', label: 'Создана', minWidth: 110 }
]
};
}
@@ -0,0 +1,53 @@
import { WebPartContext } from '@microsoft/sp-webpart-base';
// Унифицированная строка реестра (перенесено из ct-forms-list/myDocuments).
export interface DashboardRow {
typeKey: string;
id: string;
title: string;
subtitle?: string;
status: string;
statusLabel: string;
statusVariant: 'neutral' | 'info' | 'success' | 'error';
createdDate?: string;
}
export interface ColumnDef {
key: string;
label: string;
minWidth?: number;
}
export interface ApprovalSectionConfig {
approvalListName: string;
buildPendingFilter: (userId: number) => string;
approvalSelect: string;
approvalExpand?: string;
/** поле в записи согласования, хранящее ID родителя */
parentIdField: string;
buildParentQuery: (parentIds: number[]) => { listName: string; filter: string; select: string };
}
// Конфиг типа документа (CT/OID/KPS): где лежат SP-списки, как фильтровать,
// как маппить в строку и как открыть форму. Различия типов живут ЗДЕСЬ.
export interface DocumentTypeConfig {
typeKey: string;
typeLabel: string;
/** Имя иконки Fluent (легаси из myDocuments); в портале сайдбар берёт kit-иконку по typeKey. */
icon?: string;
/** Абсолютный URL сайта со списками этого типа. Не задан — текущий сайт. */
siteUrl?: string;
myDocsListName: string;
buildMyDocsFilter: (userId: number) => string;
myDocsSelect: string;
myDocsExpand?: string;
/** null = нет согласования у этого типа */
pendingApproval: ApprovalSectionConfig | null;
/** null = нет вкладки "Согласованные мной" */
approvedByMe: ApprovalSectionConfig | null;
mapItem: (raw: Record<string, unknown>, context: WebPartContext) => DashboardRow;
buildLinkUrl: (row: DashboardRow) => string;
createNewUrl?: string;
createNewLabel?: string;
columns: ColumnDef[];
}
@@ -0,0 +1,120 @@
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { SPHttpClient } from '@microsoft/sp-http';
import { DocumentTypeConfig, DashboardRow } from './documentRegistry';
// Загрузка реестра из SharePoint (перенесено из ct-forms-list/myDocuments).
// Поддерживает типы на разных сайт-коллекшенах (KPS на /sites/kps).
export class DocumentsService {
constructor(private readonly context: WebPartContext) {}
private readonly userIdCache = new Map<string, number>();
get currentUserId(): number {
return Number((this.context.pageContext.legacyPageContext as Record<string, unknown>)['userId'] ?? 0);
}
private get webUrl(): string {
return this.context.pageContext.web.absoluteUrl;
}
private siteUrlFor(config: DocumentTypeConfig): string {
return config.siteUrl ?? this.webUrl;
}
private async resolveUserId(config: DocumentTypeConfig): Promise<number> {
const site = this.siteUrlFor(config);
if (site === this.webUrl) return this.currentUserId;
const cached = this.userIdCache.get(site);
if (cached !== undefined) return cached;
const response = await this.context.spHttpClient.get(
`${site}/_api/web/currentUser?$select=Id`,
SPHttpClient.configurations.v1
);
if (!response.ok) {
throw new Error(`Не удалось определить пользователя на сайте "${site}": ${response.status}`);
}
const json: { Id: number } = await response.json();
this.userIdCache.set(site, json.Id);
return json.Id;
}
private buildListUrl(siteUrl: string, listName: string): string {
return `${siteUrl}/_api/web/lists/getByTitle('${listName}')/items`;
}
private async fetchItems(
siteUrl: string,
listName: string,
params: { filter?: string; select?: string; expand?: string; orderby?: string; top?: number }
): Promise<Record<string, unknown>[]> {
const q: string[] = [];
if (params.filter) q.push(`$filter=${params.filter}`);
if (params.select) q.push(`$select=${params.select}`);
if (params.expand) q.push(`$expand=${params.expand}`);
if (params.orderby) q.push(`$orderby=${params.orderby}`);
if (params.top !== undefined) q.push(`$top=${params.top}`);
const url = q.length > 0 ? `${this.buildListUrl(siteUrl, listName)}?${q.join('&')}` : this.buildListUrl(siteUrl, listName);
const response = await this.context.spHttpClient.get(url, SPHttpClient.configurations.v1);
if (!response.ok) {
throw new Error(`Ошибка загрузки списка "${listName}": ${response.status}`);
}
const json: { value: Record<string, unknown>[] } = await response.json();
return json.value;
}
async fetchMyDocs(config: DocumentTypeConfig): Promise<DashboardRow[]> {
const siteUrl = this.siteUrlFor(config);
const userId = await this.resolveUserId(config);
const items = await this.fetchItems(siteUrl, config.myDocsListName, {
filter: config.buildMyDocsFilter(userId),
select: config.myDocsSelect,
expand: config.myDocsExpand,
orderby: 'Created desc',
top: 100
});
return items.map((item) => config.mapItem(item, this.context));
}
async fetchApprovedByMe(config: DocumentTypeConfig): Promise<DashboardRow[]> {
if (!config.approvedByMe) return [];
return this._fetchApprovalSection(config, config.approvedByMe);
}
async fetchPendingApprovals(config: DocumentTypeConfig): Promise<DashboardRow[]> {
if (!config.pendingApproval) return [];
return this._fetchApprovalSection(config, config.pendingApproval);
}
private async _fetchApprovalSection(
config: DocumentTypeConfig,
pa: NonNullable<DocumentTypeConfig['pendingApproval']>
): Promise<DashboardRow[]> {
const siteUrl = this.siteUrlFor(config);
const userId = await this.resolveUserId(config);
const approvalItems = await this.fetchItems(siteUrl, pa.approvalListName, {
filter: pa.buildPendingFilter(userId),
select: pa.approvalSelect,
expand: pa.approvalExpand,
top: 200
});
const parentIds = Array.from(
new Set(approvalItems.map((item) => Number(item[pa.parentIdField])).filter((id) => !isNaN(id) && id > 0))
);
if (parentIds.length === 0) return [];
const parentQuery = pa.buildParentQuery(parentIds);
const parentItems = await this.fetchItems(siteUrl, parentQuery.listName, {
filter: parentQuery.filter,
select: parentQuery.select,
top: 200
});
return parentItems.map((item) => config.mapItem(item, this.context));
}
}
@@ -0,0 +1,52 @@
import { useState, useEffect, useCallback } from 'react';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { DashboardRow, DocumentTypeConfig } from './documentRegistry';
import { DocumentsService } from './documentsService';
interface IDashboardState {
myDocs: DashboardRow[];
approvedDocs: DashboardRow[];
pendingDocs: DashboardRow[];
isLoading: boolean;
error: string | null;
reload: () => void;
}
// Грузит три очереди (мои / согласованные / на согласовании) по всем типам сразу.
export function useDocumentDashboard(
context: WebPartContext,
documentTypes: DocumentTypeConfig[]
): IDashboardState {
const [myDocs, setMyDocs] = useState<DashboardRow[]>([]);
const [approvedDocs, setApprovedDocs] = useState<DashboardRow[]>([]);
const [pendingDocs, setPendingDocs] = useState<DashboardRow[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [tick, setTick] = useState<number>(0);
const reload = useCallback(() => setTick((t) => t + 1), []);
useEffect(() => {
const service = new DocumentsService(context);
setIsLoading(true);
setError(null);
Promise.all([
Promise.all(documentTypes.map((cfg) => service.fetchMyDocs(cfg))),
Promise.all(documentTypes.map((cfg) => service.fetchApprovedByMe(cfg))),
Promise.all(documentTypes.map((cfg) => service.fetchPendingApprovals(cfg)))
])
.then(([myDocGroups, approvedGroups, pendingGroups]) => {
setMyDocs(myDocGroups.reduce((acc, arr) => acc.concat(arr), [] as DashboardRow[]));
setApprovedDocs(approvedGroups.reduce((acc, arr) => acc.concat(arr), [] as DashboardRow[]));
setPendingDocs(pendingGroups.reduce((acc, arr) => acc.concat(arr), [] as DashboardRow[]));
setIsLoading(false);
})
.catch((err: Error) => {
setError(err.message ?? 'Произошла ошибка при загрузке данных');
setIsLoading(false);
});
}, [context, tick]); // eslint-disable-line react-hooks/exhaustive-deps
return { myDocs, approvedDocs, pendingDocs, isLoading, error, reload };
}
+16
View File
@@ -0,0 +1,16 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
"AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",
"AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook",
"AppSharePointEnvironment": "The app is running on SharePoint page",
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams",
"AppOfficeEnvironment": "The app is running in office.com",
"AppOutlookEnvironment": "The app is running in Outlook",
"UnknownEnvironment": "The app is running in an unknown environment"
}
});
+19
View File
@@ -0,0 +1,19 @@
declare interface ICtFormsPortalWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
AppLocalEnvironmentSharePoint: string;
AppLocalEnvironmentTeams: string;
AppLocalEnvironmentOffice: string;
AppLocalEnvironmentOutlook: string;
AppSharePointEnvironment: string;
AppTeamsTabEnvironment: string;
AppOfficeEnvironment: string;
AppOutlookEnvironment: string;
UnknownEnvironment: string;
}
declare module 'CtFormsPortalWebPartStrings' {
const strings: ICtFormsPortalWebPartStrings;
export = strings;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

+3
View File
@@ -0,0 +1,3 @@
{
"extends": "./node_modules/@microsoft/spfx-web-build-rig/profiles/default/tsconfig-base.json"
}