first commit

This commit is contained in:
João Mendes 2020-04-10 00:28:57 +01:00
parent f524f587ad
commit ca48abf13d
74 changed files with 24487 additions and 0 deletions

View File

@ -0,0 +1,13 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.10.0",
"libraryName": "personal-apps",
"libraryId": "ab3683c6-0388-47a2-84f5-90d01259d0ed",
"environment": "spo",
"packageManager": "npm",
"framework": "react",
"isCreatingSolution": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,39 @@
parameters:
# unique name of the job
job_name: deploy_sppkg
# friendly name of the job
display_name: Upload & deploy *.sppkg to SharePoint app catalog
# name of target enviroment deploying to
target_environment: ''
# app catalog scope (tenant|sitecollection)
o365cli_app_catalog_scope: 'tenant'
variable_group_name: ''
jobs:
- deployment: ${{ parameters.job_name }}
displayName: ${{ parameters.display_name }}
pool:
vmImage: 'ubuntu-latest'
environment: ${{ parameters.target_environment }}
variables:
- group: ${{parameters.variable_group_name}} #o365_user_login, o365_user_password, o365_app_catalog_site_url
strategy:
runOnce:
deploy:
steps:
- checkout: none
- download: current
artifact: drop
patterns: '**/*.sppkg'
- script: sudo npm install --global @pnp/office365-cli
displayName: Install Office365 CLI
- script: o365 login $(o365_app_catalog_site_url) --authType password --userName $(o365_user_login) --password $(o365_user_password)
displayName: Login to Office365
- script: |
CMD_GET_SPPKG_NAME=$(find $(Pipeline.Workspace)/drop -name '*.sppkg' -exec basename {} \;)
echo "##vso[task.setvariable variable=SpPkgFileName;isOutput=true]${CMD_GET_SPPKG_NAME}"
displayName: Get generated *.sppkg filename
name: GetSharePointPackage
- script: o365 spo app add --filePath "$(Pipeline.Workspace)/drop/sharepoint/solution/$(GetSharePointPackage.SpPkgFileName)" --appCatalogUrl $(o365_app_catalog_site_url) --scope ${{ parameters.o365cli_app_catalog_scope }} --overwrite
displayName: Upload SharePoint package to Site Collection App Catalog
- script: o365 spo app deploy --name $(GetSharePointPackage.SpPkgFileName) --appCatalogUrl $(o365_app_catalog_site_url) --scope ${{ parameters.o365cli_app_catalog_scope }}
displayName: Deploy SharePoint package

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"personal-apps-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/personalApps/PersonalAppsWebPart.js",
"manifest": "./src/webparts/personalApps/PersonalAppsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"PersonalAppsWebPartStrings": "lib/webparts/personalApps/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "personal-apps",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,24 @@
{
"preset": "@voitanos/jest-preset-spfx-react16",
"rootDir": "../src",
"collectCoverageFrom": [
"<rootDir>/**/*.{ts,tsx}",
"!<rootDir>/**/*.scss.*",
"!<rootDir>/loc/**/*.*"
],
"coverageReporters": [
"text",
"json",
"lcov",
"text-summary",
"cobertura"
],
"reporters": [
"default",
["jest-junit", {
"suiteName": "jest tests",
"outputDirectory": "temp/test/junit",
"outputName": "junit.xml"
}]
]
}

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "personal-apps-client-side-solution",
"id": "ab3683c6-0388-47a2-84f5-90d01259d0ed",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [{
"resource": "Microsoft Graph",
"scope": "User.ReadWrite.All"
}]
},
"paths": {
"zippedPackage": "solution/personal-apps.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,64 @@
/**
* This script updates the package-solution version analogue to the
* the package.json file.
*/
if (process.env.npm_package_version === undefined) {
throw 'Package version cannot be evaluated';
}
// define path to package-solution file
const solution = './config/package-solution.json',
teams = './teams/manifest.json';
// require filesystem instance
const fs = require('fs');
// get next automated package version from process variable
const nextPkgVersion = process.env.npm_package_version;
// make sure next build version match
const nextVersion = nextPkgVersion.indexOf('-') === -1 ?
nextPkgVersion : nextPkgVersion.split('-')[0];
// Update version in SPFx package-solution if exists
if (fs.existsSync(solution)) {
// read package-solution file
const solutionFileContent = fs.readFileSync(solution, 'UTF-8');
// parse file as json
const solutionContents = JSON.parse(solutionFileContent);
// set property of version to next version
solutionContents.solution.version = nextVersion + '.0';
// save file
fs.writeFileSync(
solution,
// convert file back to proper json
JSON.stringify(solutionContents, null, 2),
'UTF-8');
}
// Update version in teams manifest if exists
if (fs.existsSync(teams)) {
// read package-solution file
const teamsManifestContent = fs.readFileSync(teams, 'UTF-8');
// parse file as json
const teamsContent = JSON.parse(teamsManifestContent);
// set property of version to next version
teamsContent.version = nextVersion;
// save file
fs.writeFileSync(
teams,
// convert file back to proper json
JSON.stringify(teamsContent, null, 2),
'UTF-8');
}

View File

@ -0,0 +1,75 @@
# React My Personal Apps (Links)
## Summary
The Web Part My Personal Apps allows the user define links to Aplications or Sites for quick access.
This Web Part use MSgraph Open Extension to save the personal information on user object.
  
![PersonalApps](./assets/image1.png)
![PersonalApps](./assets/image11.png)
![PersonalApps](./assets/image2.png)
![PersonalApps](./assets/image3.png)
![PersonalApps](./assets/image4.png)
![PersonalApps](./assets/image5.png)
![PersonalApps](./assets/image06.png)
![PersonalApps](./assets/image7.png)
![PersonalApps](./assets/image8.png)
![PersonalApps](./assets/image9.png)
![PersonalApps](./assets/image10.png)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Solution
Solution|Author(s)
--------|---------
react My Personal Apps|João Mendes
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|April 9, 2020|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 - please follow all the steps.
- Clone this repository
- in the command line run:
- `npm install`
- `gulp build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- `Add and Deploy Package to AppCatalog `
- `Go to API Management - from SharePoint Admin Center new experience, and Approve the Permission Require to Use Graph API SCOPE User.ReadWrite.All`
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-my-personal-apps" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,67 @@
parameters:
name: ''
jobs:
- job: ${{ parameters.name }}
pool:
vmImage: 'ubuntu-latest'
demands:
- npm
- node.js
- java
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm
steps:
- checkout: self
- task: NodeTool@0
displayName: 'Use Node 10.x'
inputs:
versionSpec: 10.x
checkLatest: true
- task: CacheBeta@1
inputs:
key: npm | $(Agent.OS) | package-lock.json
path: $(npm_config_cache)
cacheHitVar: CACHE_RESTORED
- script: npm ci
displayName: 'npm ci'
- task: Gulp@0
displayName: 'Bundle project'
inputs:
targets: bundle
arguments: '--ship'
- script: npm test
displayName: 'npm test'
- task: PublishTestResults@2
displayName: Publish test results
inputs:
testResultsFormat: JUnit
testResultsFiles: '**/junit.xml'
#failTaskOnFailedTests: true #if we want to fail the build on failed unit tests
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage results'
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml'
- task: Gulp@0
displayName: 'Package Solution'
inputs:
targets: 'package-solution'
arguments: '--ship'
- task: CopyFiles@2
displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)'
inputs:
Contents: |
sharepoint/**/*.sppkg
TargetFolder: '$(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: drop'

View File

@ -0,0 +1,39 @@
parameters:
# unique name of the job
job_name: deploy_sppkg
# friendly name of the job
display_name: Upload & deploy *.sppkg to SharePoint app catalog
# name of target enviroment deploying to
target_environment: ''
# app catalog scope (tenant|sitecollection)
o365cli_app_catalog_scope: 'tenant'
variable_group_name: ''
jobs:
- deployment: ${{ parameters.job_name }}
displayName: ${{ parameters.display_name }}
pool:
vmImage: 'ubuntu-latest'
environment: ${{ parameters.target_environment }}
variables:
- group: ${{parameters.variable_group_name}} #o365_user_login, o365_user_password, o365_app_catalog_site_url
strategy:
runOnce:
deploy:
steps:
- checkout: none
- download: current
artifact: drop
patterns: '**/*.sppkg'
- script: sudo npm install --global @pnp/office365-cli
displayName: Install Office365 CLI
- script: o365 login $(o365_app_catalog_site_url) --authType password --userName $(o365_user_login) --password $(o365_user_password)
displayName: Login to Office365
- script: |
CMD_GET_SPPKG_NAME=$(find $(Pipeline.Workspace)/drop -name '*.sppkg' -exec basename {} \;)
echo "##vso[task.setvariable variable=SpPkgFileName;isOutput=true]${CMD_GET_SPPKG_NAME}"
displayName: Get generated *.sppkg filename
name: GetSharePointPackage
- script: o365 spo app add --filePath "$(Pipeline.Workspace)/drop/sharepoint/solution/$(GetSharePointPackage.SpPkgFileName)" --appCatalogUrl $(o365_app_catalog_site_url) --scope ${{ parameters.o365cli_app_catalog_scope }} --overwrite
displayName: Upload SharePoint package to Site Collection App Catalog
- script: o365 spo app deploy --name $(GetSharePointPackage.SpPkgFileName) --appCatalogUrl $(o365_app_catalog_site_url) --scope ${{ parameters.o365cli_app_catalog_scope }}
displayName: Deploy SharePoint package

View File

@ -0,0 +1,26 @@
name: $(TeamProject)_$(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
resources:
- repo: self
trigger:
branches:
include:
- master
- develop
stages:
- stage: build
displayName: build
jobs:
- template: ./azure-pipelines-build-template.yml
parameters:
name: 'buildsolution'
- stage: 'deployqa'
# uncomment if you want deployments to occur only for a specific branch
#condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
jobs:
- template: ./azure-pipelines-deploy-template.yml
parameters:
job_name: deploy_solution
target_environment: 'qa'
variable_group_name: qa_configuration

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"personal-apps-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/personalApps/PersonalAppsWebPart.js",
"manifest": "./src/webparts/personalApps/PersonalAppsWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"PersonalAppsWebPartStrings": "lib/webparts/personalApps/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "personal-apps",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,24 @@
{
"preset": "@voitanos/jest-preset-spfx-react16",
"rootDir": "../src",
"collectCoverageFrom": [
"<rootDir>/**/*.{ts,tsx}",
"!<rootDir>/**/*.scss.*",
"!<rootDir>/loc/**/*.*"
],
"coverageReporters": [
"text",
"json",
"lcov",
"text-summary",
"cobertura"
],
"reporters": [
"default",
["jest-junit", {
"suiteName": "jest tests",
"outputDirectory": "temp/test/junit",
"outputName": "junit.xml"
}]
]
}

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "personal-apps-client-side-solution",
"id": "ab3683c6-0388-47a2-84f5-90d01259d0ed",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [{
"resource": "Microsoft Graph",
"scope": "User.ReadWrite.All"
}]
},
"paths": {
"zippedPackage": "solution/personal-apps.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,54 @@
'use strict';
// check if gulp dist was called
if (process.argv.indexOf('dist') !== -1) {
// add ship options to command call
process.argv.push('--ship');
}
const path = require('path');
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const gulpSequence = require('gulp-sequence');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
// Create clean distrubution package
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
// Create clean development package
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
/**
* Webpack Bundle Anlayzer
* Reference and gulp task
*/
const bundleAnalyzer = require('webpack-bundle-analyzer');
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
const lastDirName = path.basename(__dirname);
const dropPath = path.join(__dirname, 'temp', 'stats');
generatedConfiguration.plugins.push(new bundleAnalyzer.BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerMode: 'static',
reportFilename: path.join(dropPath, `${lastDirName}.stats.html`),
generateStatsFile: true,
statsFilename: path.join(dropPath, `${lastDirName}.stats.json`),
logLevel: 'error'
}));
return generatedConfiguration;
}
});
/**
* Custom Framework Specific gulp tasks
*/
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,67 @@
{
"name": "personal-apps",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"preversion": "node ./tools/pre-version.js",
"postversion": "gulp dist",
"test": "./node_modules/.bin/jest --config ./config/jest.config.json",
"test:watch": "./node_modules/.bin/jest --config ./config/jest.config.json --watchAll"
},
"dependencies": {
"@material-ui/core": "^4.9.9",
"@material-ui/icons": "^4.9.1",
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@pnp/pnpjs": "^2.0.3",
"@pnp/spfx-controls-react": "^1.17.0",
"@pnp/spfx-property-controls": "1.16.0",
"@types/es6-promise": "0.0.33",
"@types/jquery": "^3.3.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"jquery": "^3.4.1",
"material-table": "^1.57.2",
"office-ui-fabric-react": "^7.83.1",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/microsoft-graph-types": "^1.12.0",
"@microsoft/rush-stack-compiler-3.7": "^0.2.x",
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"@types/react": "^16.9.17",
"@voitanos/jest-preset-spfx-react16": "^1.3.2",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"gulp-sequence": "1.0.0",
"jest": "^23.6.0",
"jest-junit": "^10.0.0",
"lodash": "^4.17.15",
"spfx-uifabric-themes": "^0.8.0",
"typescript": "~3.7.x",
"webpack-bundle-analyzer": "^3.6.0"
},
"jest-junit": {
"output": "temp/test/junit/junit.xml",
"usePathForSuiteName": "true"
}
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#0b0b0b",
"neutralLighter": "#151515",
"neutralLight": "#252525",
"neutralQuaternaryAlt": "#2f2f2f",
"neutralQuaternary": "#373737",
"neutralTertiaryAlt": "#595959",
"neutralTertiary": "#c8c8c8",
"neutralSecondary": "#d0d0d0",
"neutralPrimaryAlt": "#dadada",
"neutralPrimary": "#ffffff",
"neutralDark": "#f4f4f4",
"black": "#f8f8f8",
"white": "#000000"
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#2d2c2c",
"neutralLighter": "#2c2b2b",
"neutralLight": "#2a2929",
"neutralQuaternaryAlt": "#272626",
"neutralQuaternary": "#252525",
"neutralTertiaryAlt": "#242323",
"neutralTertiary": "#c8c8c8",
"neutralSecondary": "#d0d0d0",
"neutralPrimaryAlt": "#dadada",
"neutralPrimary": "#ffffff",
"neutralDark": "#f4f4f4",
"black": "#f8f8f8",
"white": "#2d2c2c"
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#ecebe9",
"neutralLighter": "#e8e7e6",
"neutralLight": "#dedddc",
"neutralQuaternaryAlt": "#cfcecd",
"neutralQuaternary": "#c6c5c4",
"neutralTertiaryAlt": "#bebdbc",
"neutralTertiary": "#b5b4b2",
"neutralSecondary": "#9d9c9a",
"neutralPrimaryAlt": "#868482",
"neutralPrimary": "#252423",
"neutralDark": "#565453",
"black": "#3e3d3b",
"white": "#f3f2f1"
}

View File

@ -0,0 +1,16 @@
$default-background: #f3f2f1;
$default-color: #252423;
$default-button-background: #6264a7;
$default-Button-color: #f3f2f1;
// dark theme
$dark-background: #2d2c2c;
$dark-color: #ffffff;
$dark-button-background: #6264a7;
$dark-button-color: #2d2c2c;
// contrast theme
$contrast-background: #000000;
$contrast-color: #ffffff;
$contrast-button-background: #b5c01c;
$contrast-Button-color: #000000;

View File

@ -0,0 +1,30 @@
export interface IIconPickerProps {
/**
* call-back function when icon selection has been confirmed
*/
onSave(iconName: string): void;
/**
* call-back function when icon has changed
*/
onChange?(iconName: string): void;
/**
* Specifies the label of the icon picker button
*/
buttonLabel?: string;
/**
* Specifies if the picker button is disabled
*/
disabled?: boolean;
/**
* Specifies a custom className for the picker button
*/
buttonClassName?: string;
/**
* Specifies a custom className for the panel element
*/
panelClassName?: string;
/**
* initially selected icon
*/
currentIcon?: string;
}

View File

@ -0,0 +1,5 @@
export interface IIconPickerState {
items: string[];
currentIcon?: string;
isPanelOpen: boolean;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,138 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.navArea {
display: flex;
width: 100%;
padding-left: 11px;
}
.headTitle {
display: inline-block;
flex-grow: 1;
flex-shrink: 2;
font-size: 28px;
font-weight: 300;
margin: auto 8px 5px 0;
white-space: nowrap;
}
.searchBox {
flex-grow: 5;
flex-shrink: 1;
margin: 5px 0;
}
.closeBtnContainer {
flex: 0 0 54px;
display: flex;
justify-content: flex-end;
}
.iconList {
list-style-type: none;
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 10px -5px;
}
.iconItem {
display: inline-block;
padding: 0;
margin: 0 2px 4px;
list-style-type: none;
position: relative;
overflow: hidden;
}
.iconRadio {
position: absolute;
left: -1000px;
opacity: 0;
}
.iconLabel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
width: 78px;
padding: 5px;
height: 70px;
border-radius: 3px;
background-color: "[theme:neutralLighterAlt, default:#f8f8f8]";
&:after {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2px solid transparent;
border-color: "[theme:neutralQuarternary, default:#d0d0d0]";
border-radius: 3px;
opacity: 0;
will-change: opacity, border-color;
transition: opacity ease-out .05s;
}
&:hover {
&:after {
opacity: 1;
}
}
}
.iconRadio:checked + .iconLabel {
&:after {
opacity: 1;
border-color: "[theme: themePrimary, default: #0078d7]";
}
color: "[theme: themePrimary, default: #0078d7]";
}
.iconRadio:focus + .iconLabel {
outline: 1px dashed;
outline-color: "[theme: themePrimary, default: #0078d7]";
outline-offset: -5px;
}
.iconGlyph {
font-size: 24px;
width: 24px;
height: 24px;
margin-bottom: 5px;
color: inherit;
}
.iconName {
display: inline-block;
max-width: 100%;
text-align: left;
font-size: 12px;
}
.footer {
display: flex;
width: 100%;
}
.selectionDisplay {
order: 2;
display: flex;
flex: 1 0 32px;
margin: 0 auto;
align-items: center;
justify-content: center;
&:global {
&.noSelection {
opacity: .3;
}
}
}
.selectionLabel {
display: inline-block;
}
.selectionIcon {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
min-width: 32px;
}
.btnCancel {
order: 1;
}
.btnSave {
order: 3;
}

View File

@ -0,0 +1,146 @@
import * as React from 'react';
import { IIconPickerProps } from '.';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
import { IRenderFunction, getId } from 'office-ui-fabric-react/lib/Utilities';
import styles from './IconPicker.module.scss';
import * as strings from "PersonalAppsWebPartStrings";
import { IconNames } from './IconNames';
import { Panel, PanelType, IPanelProps } from 'office-ui-fabric-react/lib/Panel';
import { debounce } from 'lodash';
import { IIconPickerState } from './IIconPickerState';
export class IconPicker extends React.Component<IIconPickerProps, IIconPickerState> {
private radioIdBase: string = getId("radio");
constructor(props: IIconPickerProps) {
super(props);
this.state = {
items: IconNames.Icons,
isPanelOpen: false,
currentIcon: this.props.currentIcon || null
};
}
public render(): React.ReactElement<IIconPickerProps> {
return <div>
<PrimaryButton
text={this.props.buttonLabel}
onClick={this.iconPickerOnClick}
className={this.props.buttonClassName}
disabled={this.props.disabled}
data-automation-id={`icon-picker-open`}
/>
<Panel
isOpen={this.state.isPanelOpen}
onDismiss={this.closePanel}
type={PanelType.medium}
data-automation-id={`icon-picker-panel`}
closeButtonAriaLabel={strings.IconPickerCloseLabel}
className={this.props.panelClassName}
onRenderNavigation={this.renderPanelNav}
onRenderFooterContent={this.renderPanelFooter}
>
{this.renderPanelContent()}
</Panel>
</div>;
}
private closePanel = (): void => {
this.setState({
currentIcon: null,
isPanelOpen: false
});
}
private iconPickerOnClick = (): void => {
this.setState({
isPanelOpen: true
});
}
private iconOnClick = (iconName: string): void => {
if (this.props.onChange) this.props.onChange(iconName);
this.setState({
currentIcon: iconName,
});
}
private onAbort = (): void => {
this.setState({ items: IconNames.Icons });
}
private onChange = (event?: React.ChangeEvent<HTMLInputElement>, newValue?: string): void => {
let items: string[];
if (newValue && newValue.trim().length > 2) {
items = IconNames.Icons.filter(item => {
return item.toLocaleLowerCase().indexOf(newValue.toLocaleLowerCase()) !== -1;
});
} else {
items = IconNames.Icons;
}
this.setState({
items: items
});
}
private confirmSelection = (): void => {
if (this.props.onSave) this.props.onSave(this.state.currentIcon);
this.setState({
isPanelOpen: false,
});
}
private renderPanelNav: IRenderFunction<IPanelProps> = (props: IPanelProps, defaultRender: IRenderFunction<IPanelProps>) => {
return <div className={styles.navArea}>
<h2 className={styles.headTitle}>{strings.IconPickerSelectLabel}</h2>
<SearchBox className={styles.searchBox}
onAbort={this.onAbort}
data-automation-id={`icon-picker-search`}
onChange={this.onChange} />
<div className={styles.closeBtnContainer}>{defaultRender!(props)}</div>
</div>;
}
private renderPanelContent = () => {
return <div>
{this.renderIcons()}
</div>;
}
private renderPanelFooter: IRenderFunction<IPanelProps> = () => {
return <div className={styles.footer} data-automation-id={`icon-picker-footer`}>
<PrimaryButton text={strings.IconPickerSaveLabel} onClick={this.confirmSelection} disabled={!this.state.currentIcon} className={styles.btnSave} data-automation-id={`icon-picker-save`} />
<div className={`${styles.selectionDisplay} ${!this.state.currentIcon ? 'noSelection' : ''}`}>
<span className={styles.selectionLabel}>{strings.IconPickerSelectedLabel}:</span>
<Icon iconName={this.state.currentIcon} className={styles.selectionIcon} />
</div>
<DefaultButton text={strings.IconPickerCancelLabel} onClick={this.closePanel} className={styles.btnCancel} data-automation-id={`icon-picker-close`} />
</div>;
}
private renderIcons = (): React.ReactElement<IIconPickerProps> => {
return (<ul className={styles.iconList}>
{this.state.items.map(this.renderIcon)}
</ul>);
}
private renderIcon = (item: string): JSX.Element => {
const radioId: string = `${this.radioIdBase}-${item}`;
return <li className={styles.iconItem}>
<input type="radio" name={this.radioIdBase} id={radioId} className={styles.iconRadio}
data-automation-id={`icon-picker-${item}`}
checked={item == this.state.currentIcon}
onChange={() => this.iconOnClick(item)} />
<label className={styles.iconLabel} htmlFor={radioId} title={item}>
<Icon iconName={item} className={styles.iconGlyph} />
<span className={styles.iconName}>{item}</span>
</label>
</li>;
}
}

View File

@ -0,0 +1,2 @@
export * from './IIconPickerProps';
export * from './IconPicker';

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,6 @@
export interface IAppItem {
title:string;
name:string;
description: string;
iconName: string;
}

View File

@ -0,0 +1,6 @@
export interface ITenantProperty {
key: string;
Comment?: string;
Description?: string;
Value: string;
}

View File

@ -0,0 +1,74 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import {
MSGraphClient,
} from "@microsoft/sp-http";
import * as MicrosoftGraph from "@microsoft/microsoft-graph-types";
import { IListItem } from "../webparts/personalApps/components/ManageApps/IListItem";
export default class dataservices {
private static _MSGraphClient: MSGraphClient;
private static _hasExtension: boolean = false;
/*
initialize the static class
*/
public static async init(context: WebPartContext) {
//obtain the httpClient from the webpart context
this._MSGraphClient = await context.msGraphClientFactory.getClient();
}
// Get Sources
public static async getUserApps(): Promise<IListItem[]> {
try {
let _myApps = await this._MSGraphClient
.api(`/me/extensions/MyApps`)
.get();
this._hasExtension = true;
return _myApps ? _myApps.Apps : [];
} catch (error) {
console.log (error);
return [];
}
}
public static async createOrUpdateUserApps(
listApps: IListItem[]
): Promise<microsoftgraph.OpenTypeExtension> {
try {
let _extensionResult: any;
let extentionData: Object = {};
// User has extention created ?
if (this._hasExtension) {
extentionData = {
Apps: listApps
};
// Call the REST API
_extensionResult = await this._MSGraphClient
.api(`/me/extensions/MyApps`)
.patch(extentionData);
} else {
// Create Extention with Data
extentionData = {
"@odata.type": "#microsoft.graph.openTypeExtension",
extensionName: "MyApps",
Apps: listApps
};
// Call the Rest API
_extensionResult = await this._MSGraphClient
.api(`/me/extensions`)
.post(extentionData);
// Flag user has Extention crerated
this._hasExtension = true;
}
return _extensionResult;
} catch (error) {
console.log(error);
throw new Error("Error create or Update Extention");
}
return;
}
}

View File

@ -0,0 +1,28 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "4dfcaee3-8a85-4d80-9fa3-23bcb1e08923",
"alias": "PersonalAppsWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
"supportsThemeVariants": true,
// 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","TeamsTab","TeamsPersonalApp","SharePointFullPage"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "My Personal Apps and Links" },
"description": { "default": "My Personal Apps and Links " },
"officeFabricIconFontName": "AppIconDefaultList",
"properties": {
"title": "My Personal Apps",
"view": "List"
}
}]
}

View File

@ -0,0 +1,156 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneChoiceGroup
} from "@microsoft/sp-property-pane";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { DisplayMode } from "@microsoft/sp-core-library";
import * as strings from "PersonalAppsWebPartStrings";
import PersonalApps from "./components/PersonalApps";
import { IPersonalAppsProps } from "./components/IPersonalAppsProps";
import {
PropertyFieldCollectionData,
CustomCollectionFieldType
} from "@pnp/spfx-property-controls/lib/PropertyFieldCollectionData";
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme } from '@microsoft/sp-component-base';
import dataservices from "../../services/dataservices";
import { loadTheme } from "office-ui-fabric-react";
const teamsDefaultTheme = require("../../common/TeamsDefaultTheme.json");
const teamsDarkTheme = require("../../common/TeamsDarkTheme.json");
const teamsContrastTheme = require("../../common/TeamsContrastTheme.json");
export interface IPersonalAppsWebPartProps {
title: string;
view: string | number;
}
export default class PersonalAppsWebPart extends BaseClientSideWebPart<
IPersonalAppsWebPartProps
> {
private _themeProvider: ThemeProvider;
private _themeVariant: IReadonlyTheme | undefined;
protected async onInit<T>(): Promise<T> {
await dataservices.init(this.context);
this._themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
// If it exists, get the theme variant
this._themeVariant = this._themeProvider.tryGetTheme();
// Register a handler to be notified if the theme variant changes
this._themeProvider.themeChangedEvent.add(this, this._handleThemeChangedEvent);
if (this.context.sdks.microsoftTeams) {
// in teams ?
const context = this.context.sdks.microsoftTeams!.context;
this._applyTheme(context.theme || "default");
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(
this._applyTheme
);
}
return Promise.resolve();
}
/**
* Update the current theme variant reference and re-render.
*
* @param args The new theme
*/
private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
this._themeVariant = args.theme;
this.render();
}
// Apply btheme id in Teams
private _applyTheme = (theme: string): void => {
this.context.domElement.setAttribute("data-theme", theme);
document.body.setAttribute("data-theme", theme);
if (theme == "dark") {
loadTheme({
palette: teamsDarkTheme
});
}
if (theme == "default") {
loadTheme({
palette: teamsDefaultTheme
});
}
if (theme == "contrast") {
loadTheme({
palette: teamsContrastTheme
});
}
}
public render(): void {
const element: React.ReactElement<IPersonalAppsProps> = React.createElement(
PersonalApps,
{
title: this.properties.title,
view: this.properties.view,
displayMode: this.displayMode,
themeVariant: this._themeVariant,
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
const { view } = this.properties;
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("title", {
label: strings.DescriptionFieldLabel
}),
PropertyPaneChoiceGroup("view", {
label: "view option",
options: [
{
key: "List",
text: "List",
iconProps: { officeFabricIconFontName: "List" },
checked: view === "List" ? true : false
},
{
key: "Tiles",
text: "Tiles",
iconProps: { officeFabricIconFontName: "Tiles" },
checked: view === "Tiles" ? true : false
}
]
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,120 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@import "~office-ui-fabric-react/dist/sass/semanticSlots";
@import './node_modules/spfx-uifabric-themes/office.theme';
@import "./../../../../Common/themeColors.module.scss";
@media screen and (min-width: 40em) {
.card {
max-width: calc(50% - 1em);
}
}
@media screen and (min-width: 60em) {
.card {
max-width: calc(25% - 1em);
}
}
@media all and (min-width: 480px) and (max-width: 768px) {
.card {
max-width: 100%;
}
}
.card {
display: flex;
min-height: 60px;
max-height: 60px;
min-width: 300px;
justify-content: start;
margin-bottom: 5px;
margin-right: 5px;
flex-direction: row;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralTertiaryAlt;
padding: 20px;
align-items: flex-start;
background-color:$bodyBackgroundColor;
color: $bodyTextColor;
flex: 0 1 365px;
}
.card:hover {
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
background-color: $ms-themePrimary;
color: $ms-color-white;
cursor: pointer;
}
.image {
font-size: 40px;
width: 40px;
height: 40px;
}
.imageContainer {
display: flex;
margin-right: 30px;
align-items: center;
width: 45px;
height: 60px;
}
.title {
font-size: 16px;
font-weight: 300px;
line-height: 25px;
line-break: nowrap;
overflow: hidden;
text-overflow: 'ellipsis';
}
.separator {
border-color: $ms-color-themePrimary;
width: 90%;
margin: 0 auto;
height: 0px;
border-style: solid;
border-width: 1px;
}
[data-theme="dark"] {
color: $ms-color-neutralTertiaryAlt;
}
[data-theme="contrast"] {
// dark theme
color: $contrast-button-background;
.card:hover{
color: $ms-color-black;
background-color: $contrast-button-background;
}
}
[data-theme="default"] {
.card{
background-color: $ms-color-white;
}
.card:hover {
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
background-color: $ms-themePrimary;
color: $ms-color-white;
cursor: pointer;
}
}
/*
[data-theme="default"] {
$default-background: #f3f2f1;
$default-color: #252423;
$default-button-background: #6264a7;
$default-Button-color: #f3f2f1;
color: $default-color;
}*/

View File

@ -0,0 +1,34 @@
import {
FontIcon,
Text,
} from "office-ui-fabric-react";
import { IAppItemProps } from "./IAppItemProps";
import * as React from "react";
import styles from "./AppItem.module.scss";
export const AppItem = (props: IAppItemProps) => {
return (
<>
<div
className={styles.card}
onClick={event => {
event.preventDefault();
window.open(props.url, "_blank");
}}
>
<div className={styles.imageContainer}>
<FontIcon
iconName={props.iconName}
className={styles.image}
></FontIcon>
</div>
<div>
<div className={styles.title}>{props.title}</div>
<Text variant="small" block>
{props.description}
</Text>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,6 @@
export interface IAppItemProps {
title:string;
description: string;
iconName: string;
url:string;
}

View File

@ -0,0 +1,88 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@import "./node_modules/spfx-uifabric-themes/office.theme";
@import "./../../../../Common/themeColors.module.scss";
.tile {
display: flex;
max-width: 120px;
min-height: 120px;
max-height: 120px;
min-width: 120px;
justify-content: start;
margin: 5px;
flex-direction: column;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralTertiaryAlt;
padding: 10px;
align-items: center;
justify-items: center;
justify-content: center;
}
.tile:hover {
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
background-color: $ms-themePrimary;
color: $ms-color-white;
cursor: pointer;
}
.image {
font-size: 48px;
width: 48px;
height: 48px;
}
.imageContainer {
display: flex;
align-items: center;
width: 48px;
height: 48px;
margin-bottom: 5px;
}
.title {
font-size: 16px;
font-weight: 600px;
line-height: 25px;
line-break: nowrap;
overflow: hidden;
text-overflow: "ellipsis";
}
.separator {
border-color: $ms-color-themePrimary;
width: 90%;
margin: 0 auto;
height: 0px;
border-style: solid;
border-width: 1px;
}
[data-theme="default"] {
.tile {
background-color: $ms-color-white;
}
.tile:hover {
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
background-color: $ms-themePrimary;
color: $ms-color-white;
cursor: pointer;
}
}
[data-theme="dark"] {
color: $ms-color-neutralTertiaryAlt;
}
[data-theme="contrast"] {
// dark theme
color: $contrast-button-background;
.tile:hover {
color: $ms-color-black;
background-color: $contrast-button-background;
}
}

View File

@ -0,0 +1,29 @@
import { FontIcon, Text } from "office-ui-fabric-react";
import { IAppTileProps } from "./IAppTileProps";
import * as React from "react";
import * as ReactDom from "react-dom";
import styles from "./AppTile.module.scss";
export const AppTile = (props: IAppTileProps) => {
return (
<>
<div
className={styles.tile}
onClick={event => {
event.preventDefault();
window.open(props.url, "_blank");
}}
>
<div className={styles.imageContainer}>
<FontIcon
iconName={props.iconName}
className={styles.image}
></FontIcon>
</div>
<div>
<div className={styles.title}>{props.title}</div>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,6 @@
export interface IAppTileProps {
title:string;
description: string;
iconName: string;
url:string;
}

View File

@ -0,0 +1,8 @@
import { DisplayMode } from "@microsoft/sp-core-library";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface IPersonalAppsProps {
title: string;
view:string | number;
displayMode: DisplayMode;
themeVariant: IReadonlyTheme | undefined;
}

View File

@ -0,0 +1,9 @@
import { IListItem } from "./ManageApps/IListItem";
export interface IPersonalAppsState {
showPanel: boolean;
apps : IListItem[];
isLoading: boolean;
hasError: boolean;
errorMessage: string;
}

View File

@ -0,0 +1,6 @@
export interface IListItem {
name:string;
description:string;
url:string;
iconName:string;
}

View File

@ -0,0 +1,7 @@
import { IListItem } from "./IListItem";
export interface IManageAppsProps {
showPanel: boolean;
onDismiss: (list: IListItem[], changed:boolean) => void;
Apps: IListItem[];
}

View File

@ -0,0 +1,7 @@
import { IManageAppsProps } from "./IManageAppsProps";
export interface IManageAppsState {
columns: any[];
data: IManageAppsProps[];
}

View File

@ -0,0 +1,171 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@import "./node_modules/spfx-uifabric-themes/office.theme";
@import "./../../../../Common/themeColors.module.scss";
@import "~office-ui-fabric-react/dist/sass/semanticSlots";
.MuiPaperRoot {
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
min-height: 100%;
opacity: 0.8;
}
.overlay {
display: flex;
justify-content: center;
width: "100%";
}
:global {
[data-theme="default"] {
.MuiTableCell-head {
background-color: inherit !important;
}
.MuiTable-root {
background-color: inherit!important;
}
.MuiIconButton-root:hover {
background-color: inherit;
color: inherit !important;
}
.MuiTableCell-body {
color: inherit;
}
}
[data-theme="dark"] {
.MuiTableCell-head {
background-color: $dark-background !important;
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiButtonBase-root {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiTextField-root {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiTablePagination-toolbar {
color: $ms-color-neutralTertiaryAlt !important;
}
.ms-Button--default {
background-color: $ms-color-neutralTertiary;
}
.MuiInput-underline:before {
border-bottom-color: $ms-color-neutralTertiaryAlt !important;
}
.MuiSelect-root {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiTypography-caption {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiSelect-icon {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiInputBase-root {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiTable-root {
background-color: $dark-background !important;
}
.MuiIconButton-root:hover {
background-color: $ms-themePrimary;
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiTableCell-body {
color: inherit;
}
}
[data-theme="contrast"] {
.MuiButtonBase-root {
color: $contrast-button-background !important;
}
.ms-Button--default {
background-color: $ms-color-black;
}
.ms-Button--Primary {
color: $ms-color-black;
background-color: $contrast-button-background;
}
.MuiTableCell-head {
background-color: $contrast-background !important;
color: $contrast-button-background !important;
}
.MuiButtonBase-root {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiTextField-root {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiTablePagination-toolbar {
color: $ms-color-neutralTertiaryAlt !important;
}
.ms-Button--default {
background-color: $ms-color-neutralTertiary;
}
.MuiInput-underline:before {
border-bottom-color: $ms-color-neutralTertiaryAlt !important;
}
.MuiSelect-root {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiTypography-caption {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiSelect-icon {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiInputBase-root {
color: $ms-color-neutralTertiaryAlt !important;
}
.MuiTable-root {
background-color: $contrast-background !important;
}
.MuiIconButton-root:hover {
background-color: $contrast-button-background;
color: $contrast-background !important;
}
.MuiTableCell-body {
color: inherit;
}
}
}
[data-theme="dark"] {
.MuiPaperRoot {
background-color: $dark-background !important;
color: $ms-color-neutralTertiaryAlt !important;
}
.overlay {
background-color: $dark-background !important;
display: flex;
justify-content: center;
width: "100%";
}
}
[data-theme="contrast"] {
.MuiPaperRoot {
// dark theme
background-color: $contrast-background !important;
color: $contrast-button-background !important;
.overlay {
background-color: $contrast-background !important;
}
}
}
[data-theme="default"] {
.MuiPaperRoot {
// dark theme
background-color: $default-background !important;
}
.overlay {
background-color: $default-background !important;
opacity: 0.6;
display: flex;
justify-content: center;
width: "100%";
height: "100%";
align-items: center´;
}
}

View File

@ -0,0 +1,331 @@
import * as React from "react";
import { useState, useEffect } from "react";
import { IManageAppsProps } from "../ManageApps/IManageAppsProps";
import { IManageAppsState } from "../ManageApps/IManageAppsState";
import MaterialTable, { Icons, MTableHeader } from "material-table";
import dataservices from "../../../../services/dataservices";
import * as strings from "PersonalAppsWebPartStrings";
import { IconPicker } from "../../../../controls/iconPicker";
import {
FontIcon,
TextField,
Spinner,
SpinnerSize,
SpinnerType,
Panel,
PanelType,
Text,
Label,
MessageBar,
MessageBarType,
PrimaryButton,
DefaultButton
} from "office-ui-fabric-react";
import Check from "@material-ui/icons/Check";
import ChevronLeft from "@material-ui/icons/ChevronLeft";
import ChevronRight from "@material-ui/icons/ChevronRight";
import Clear from "@material-ui/icons/Clear";
import DeleteOutline from "@material-ui/icons/DeleteOutline";
import Edit from "@material-ui/icons/Edit";
import FilterList from "@material-ui/icons/FilterList";
import FirstPage from "@material-ui/icons/FirstPage";
import LastPage from "@material-ui/icons/LastPage";
import Remove from "@material-ui/icons/Remove";
import SaveAlt from "@material-ui/icons/SaveAlt";
import Search from "@material-ui/icons/Search";
import ViewColumn from "@material-ui/icons/ViewColumn";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { IListItem } from "./IListItem";
import { Paper, CircularProgress } from "@material-ui/core";
import styles from "./ManageApps.module.scss";
initializeIcons();
const tableIcons: Icons = {
Add: React.forwardRef((props, ref) => (
<FontIcon iconName="AppIconDefaultAdd" style={{ fontWeight: 700 }} />
)),
Check: React.forwardRef((props, ref) => <Check />),
Clear: React.forwardRef((props, ref) => <Clear />),
Delete: React.forwardRef((props, ref) => <FontIcon iconName="Delete" style={{fontSize: 20}} />),
DetailPanel: React.forwardRef((props, ref) => <ChevronRight />),
Edit: React.forwardRef((props, ref) => <FontIcon iconName="Edit" style={{fontSize: 20}} />),
Export: React.forwardRef((props, ref) => <SaveAlt />),
Filter: React.forwardRef((props, ref) => <FilterList />),
FirstPage: React.forwardRef((props, ref) => <FirstPage />),
LastPage: React.forwardRef((props, ref) => <LastPage />),
NextPage: React.forwardRef((props, ref) => <ChevronRight />),
PreviousPage: React.forwardRef((props, ref) => <ChevronLeft />),
ResetSearch: React.forwardRef((props, ref) => <Clear />),
Search: React.forwardRef((props, ref) => <Search />),
SortArrow: React.forwardRef((props, ref) => (
<FontIcon iconName="Sort" style={{ marginLeft: 5 }} />
)),
ThirdStateCheck: React.forwardRef((props, ref) => <Remove />),
ViewColumn: React.forwardRef((props, ref) => <ViewColumn />)
};
export function ManageApps(appProps: IManageAppsProps) {
const [isLoading, setIsLoading] = useState(false);
const [changeData, setChangeData] = useState(false);
const [hasError, setHasError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [showPanel, setShowPanel] = useState(appProps.showPanel);
const [isSaving, setIsSaving] = useState(false);
const [state, setState] = useState({
columns: [
{
title: "Name ",
field: "name",
editComponent: props => (
<TextField
underlined
required
placeholder="Enter Name here"
onGetErrorMessage={(newValue: string) => {
return newValue.trim().length > 0 ? "" : "Please enter Name";
}}
validateOnFocusOut
validateOnLoad={false}
value={props.value}
onChange={(event: React.FormEvent<HTMLInputElement>, newValue) => {
props.onChange(newValue);
}}
/>
)
},
{
title: "Derscription",
field: "description",
editComponent: props => (
<TextField
underlined
required
placeholder="Enter description here"
validateOnFocusOut
validateOnLoad={false}
onGetErrorMessage={(newValue: string) => {
return newValue.trim().length > 0
? ""
: "Please enter Description";
}}
value={props.value}
onChange={(event: React.FormEvent<HTMLInputElement>, newValue) => {
props.onChange(newValue);
}}
/>
)
},
{
title: "Url",
field: "Url",
editComponent: props => (
<TextField
underlined
required
placeholder="Enter URL here"
onGetErrorMessage={(newValue: string) => {
try {
const _URL = new URL(newValue);
return "";
} catch (error) {
return "Please enter valid Url";
}
}}
validateOnFocusOut
validateOnLoad={false}
value={props.value}
onChange={(event: React.FormEvent<HTMLInputElement>, newValue) => {
props.onChange(newValue);
}}
/>
)
},
{
title: "Icon",
field: "iconName",
render: rowData => (
<FontIcon
iconName={rowData.iconName}
style={{ width: 24, height: 24, fontSize: 24 }}
/>
),
editComponent: props => (
<div style={{ display: "Flex", flexDirection: "row" }}>
{" "}
<FontIcon
iconName={props.value}
style={{ width: 24, height: 24, fontSize: 24, marginRight: 7 }}
/>
<IconPicker
buttonLabel={" select Icon"}
currentIcon={props.value}
onSave={(iconName: string) => {
props.onChange(iconName);
}}
/>
</div>
)
}
],
data: appProps.Apps
});
// Load Schema Extension Data
useEffect(() => {
(async () => {
// Get Tenant Property with id of Extension Id to check if exists or needs to create
})();
});
// Cancel command
const _onDismiss = async () => {
appProps.onDismiss(state.data, false);
};
// Save command
const _onSave = async () => {
try {
setIsSaving(true);
const _result: microsoftgraph.OpenTypeExtension = await dataservices.createOrUpdateUserApps(
state.data
);
console.log("extention created or updated", _result);
appProps.onDismiss(state.data, true);
} catch (error) {
setHasError(true);
setErrorMessage(error.message);
}
};
// Render Panel commands
const _onRenderFooterContent = () => (
<div
style={{
display: "flex",
justifyContent: "flex-end",
width: "100%",
marginBottom: 35
}}
>
<PrimaryButton
onClick={_onSave}
disabled={isSaving}
style={{ marginRight: 7, width: 100 }}
>
{isSaving ? (
<Spinner size={SpinnerSize.xSmall}></Spinner>
) : (
strings.SaveLabelButtom
)}
</PrimaryButton>
<DefaultButton style={{ width: 100 }} onClick={_onDismiss}>
{strings.CancelLabelButton}
</DefaultButton>
</div>
);
return (
<Panel
isOpen={showPanel}
onDismiss={_onDismiss}
type={PanelType.custom}
customWidth="888px"
closeButtonAriaLabel="Close"
headerText="My Apps"
onRenderFooterContent={_onRenderFooterContent}
isFooterAtBottom={true}
>
<div style={{ marginTop: 20, marginBottom: 25 }}>
<Text variant="large" block>
Please add links for your favorite Apps
</Text>
</div>
{hasError && (
<MessageBar messageBarType={MessageBarType.error}>
{errorMessage}
</MessageBar>
)}
{isLoading ? (
<Spinner size={SpinnerSize.medium} />
) : (
<div style={{ height: "100%" }}>
<MaterialTable
title="My Apps"
isLoading={false}
columns={state.columns}
components={{
OverlayLoading: props => (
<div className={styles.overlay}><CircularProgress /></div>
),
Container: props => (
<Paper
{...props}
elevation={0}
classes={{ root: styles.MuiPaperRoot }}
/>
)
}}
data={state.data}
icons={tableIcons}
options={{
paging: true,
showTitle:false,
searchFieldAlignment:'left',
pageSize: 7,
search: true,
minBodyHeight: "100%"
}}
editable={{
onRowAdd: (newData: IListItem) =>
new Promise(resolve => {
setTimeout(() => {
resolve();
setChangeData(true);
setState(prevState => {
const data = [...prevState.data];
data.push(newData);
return { ...prevState, data };
});
}, 600);
}),
onRowUpdate: (newData, oldData) =>
new Promise(resolve => {
setTimeout(() => {
resolve();
if (oldData) {
setChangeData(true);
setState(prevState => {
const data = [...prevState.data];
data[data.indexOf(oldData)] = newData;
return { ...prevState, data };
});
}
}, 600);
}),
onRowDelete: oldData =>
new Promise(resolve => {
setTimeout(() => {
resolve();
setChangeData(true);
setState(prevState => {
const data = [...prevState.data];
data.splice(data.indexOf(oldData), 1);
return { ...prevState, data };
});
}, 600);
})
}}
/>
</div>
)}
</Panel>
);
}

View File

@ -0,0 +1,90 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@import "./node_modules/spfx-uifabric-themes/office.theme";
@import "~office-ui-fabric-react/dist/sass/semanticSlots";
.imageSetting {
width: 28px;
height: 28px;
font-size: 28px;
font-weight: 700;
}
.imageSetting:hover {
cursor: pointer;
}
.title {
@include ms-font-xl;
}
.personalApps {
padding-top: 15px;
.container {
margin-top:15px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,159 @@
import * as React from "react";
import styles from "./PersonalApps.module.scss";
import { IPersonalAppsProps } from "./IPersonalAppsProps";
import { IPersonalAppsState } from "./IPersonalAppsState";
import { escape } from "@microsoft/sp-lodash-subset";
import { AppItem } from "../components/AppItem/AppItem";
import { AppTile } from "../components/AppTile/AppTile";
import { FontIcon, Label } from "office-ui-fabric-react";
import { ManageApps } from "../components/ManageApps/ManageApps";
import { IListItem } from "./ManageApps/IListItem";
import dataservices from "../../../services/dataservices";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { DisplayMode } from "@microsoft/sp-core-library";
import { Customizer } from '@uifabric/utilities/lib/';
import {
Spinner,
SpinnerSize,
MessageBar,
MessageBarType
} from "office-ui-fabric-react";
import { FormHelperText } from "@material-ui/core";
import strings from "PersonalAppsWebPartStrings";
export default class PersonalApps extends React.Component<
IPersonalAppsProps,
IPersonalAppsState
> {
constructor(props: IPersonalAppsProps) {
super(props);
this.state = {
showPanel: false,
apps: [],
isLoading: false,
hasError: false,
errorMessage: ""
};
}
public async componentDidMount(): Promise<void> {
this.setState({ isLoading: true });
try {
const _listApps = await dataservices.getUserApps();
this.setState({
apps: _listApps,
isLoading: false,
hasError: false,
errorMessage: ""
});
} catch (error) {
this.setState({ hasError: true, errorMessage: error.message });
}
}
private _onPanelDismiss = (apps: IListItem[], changed: boolean) => {
if (changed) {
this.setState({
apps: apps,
showPanel: false
});
} else {
this.setState({
showPanel: false
});
}
}
public render(): React.ReactElement<IPersonalAppsProps> {
const { apps, isLoading, hasError, errorMessage } = this.state;
const { view } = this.props;
return (
<>
<Customizer settings={{ theme: this.props.themeVariant }}>
<div
style={{
display: "flex",
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
}}
>
<Label className={styles.title}>{this.props.title}</Label>
<FontIcon
iconName="PlayerSettings"
title={"My Apps Settings"}
className={styles.imageSetting}
onClick={event => {
event.preventDefault();
this.setState({ showPanel: true });
}}
/>
</div>
<div className={styles.personalApps}>
{apps && apps.length == 0 && !isLoading && (
<Placeholder
iconName="AppIconDefaultList"
iconText={strings.PlaceholderIconText}
description={strings.PlaceHolderDescription}
buttonLabel={strings.PlaceHolderButtonLabel}
onConfigure={() => {
this.setState({ showPanel: true });
}}
/>
)}
<div className={styles.container}>
{isLoading && (
<div
style={{
display: "flex",
justifyContent: "center",
width: "100%"
}}
>
<Spinner size={SpinnerSize.medium} />
</div>
)}
{hasError && (
<MessageBar messageBarType={MessageBarType.error}>
{errorMessage}
</MessageBar>
)}
{apps &&
apps.length > 0 &&
apps.map(item => {
return (
<>
{view == "Tiles" ? (
<AppTile
title={item.name}
description={item.description}
iconName={item.iconName}
url={item.url}
/>
) : (
<AppItem
title={item.name}
description={item.description}
iconName={item.iconName}
url={item.url}
/>
)}
</>
);
})}
{this.state.showPanel && (
<ManageApps
onDismiss={this._onPanelDismiss}
showPanel={this.state.showPanel}
Apps={this.state.apps}
/>
)}
</div>
</div>
</Customizer>
</>
);
}
}

View File

@ -0,0 +1,17 @@
define([], function() {
return {
IconPickerCancelLabel: "Cancel",
IconPickerSelectedLabel: "Selected",
IconPickerSaveLabel: "Save\"",
IconPickerSelectLabel: "Select an Icon",
IconPickerCloseLabel: "Close",
PlaceHolderButtonLabel: "Add",
PlaceHolderDescription: "Please add your favorite Apps ",
PlaceholderIconText: "You don't have any favorite Apps",
CancelLabelButton: "Cancel",
SaveLabelButtom: "Save",
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,20 @@
declare interface IPersonalAppsWebPartStrings {
IconPickerCancelLabel: string;
IconPickerSelectedLabel: string;
IconPickerSaveLabel: string;
IconPickerSelectLabel: string;
IconPickerCloseLabel: string;
PlaceHolderButtonLabel: string;
PlaceHolderDescription: string;
PlaceholderIconText: string;
CancelLabelButton: string;
SaveLabelButtom: string;
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'PersonalAppsWebPartStrings' {
const strings: IPersonalAppsWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,47 @@
{
  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.2/MicrosoftTeams.schema.json",
  "manifestVersion": "1.2",
  "packageName": "personal-apps",
  "id": "ab3683c6-0388-47a2-84f5-90d01259d0ed",
  "version": "0.1",
  "developer": {
    "name": "Dev",
    "websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration",
    "privacyUrl": "https://privacy.microsoft.com/en-us/privacystatement",
    "termsOfUseUrl": "https://www.microsoft.com/en-us/servicesagreement"
  },
  "name": {
    "short": "My Personal Apps"
  },
  "description": {
    "short": "My Personal Apps",
    "full": "My Personal Apps "
  },
  "icons": {
    "outline": "4dfcaee3-8a85-4d80-9fa3-23bcb1e08923_outline.png",
    "color": "4dfcaee3-8a85-4d80-9fa3-23bcb1e08923_color.png"
  },
  "accentColor": "#004578",
  "configurableTabs": [
    {
      "configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=ab3683c6-0388-47a2-84f5-90d01259d0ed",
      "canUpdateConfiguration": false,
      "scopes": [
        "team"
      ]
    }
  ],
  "validDomains": [
    "*.login.microsoftonline.com",
    "*.sharepoint.com",
    "*.sharepoint-df.com",
    "spoppe-a.akamaihd.net",
    "spoprod-a.akamaihd.net",
    "resourceseng.blob.core.windows.net",
    "msft.spoppe.com"
  ],
  "webApplicationInfo": {
    "resource": "https://{teamSiteDomain}",
    "id": "00000003-0000-0ff1-ce00-000000000000"
  }
}

View File

@ -0,0 +1,64 @@
/**
* This script updates the package-solution version analogue to the
* the package.json file.
*/
if (process.env.npm_package_version === undefined) {
throw 'Package version cannot be evaluated';
}
// define path to package-solution file
const solution = './config/package-solution.json',
teams = './teams/manifest.json';
// require filesystem instance
const fs = require('fs');
// get next automated package version from process variable
const nextPkgVersion = process.env.npm_package_version;
// make sure next build version match
const nextVersion = nextPkgVersion.indexOf('-') === -1 ?
nextPkgVersion : nextPkgVersion.split('-')[0];
// Update version in SPFx package-solution if exists
if (fs.existsSync(solution)) {
// read package-solution file
const solutionFileContent = fs.readFileSync(solution, 'UTF-8');
// parse file as json
const solutionContents = JSON.parse(solutionFileContent);
// set property of version to next version
solutionContents.solution.version = nextVersion + '.0';
// save file
fs.writeFileSync(
solution,
// convert file back to proper json
JSON.stringify(solutionContents, null, 2),
'UTF-8');
}
// Update version in teams manifest if exists
if (fs.existsSync(teams)) {
// read package-solution file
const teamsManifestContent = fs.readFileSync(teams, 'UTF-8');
// parse file as json
const teamsContent = JSON.parse(teamsManifestContent);
// set property of version to next version
teamsContent.version = nextVersion;
// save file
fs.writeFileSync(
teams,
// convert file back to proper json
JSON.stringify(teamsContent, null, 2),
'UTF-8');
}

View File

@ -0,0 +1,40 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.7/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": [
"src/**/*.ts", "src/webparts/personalApps/components/AppItem/IAppItem.tsx"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}